Browse Source

Merge branch 'dev' into rfc9773-ari

pull/6548/head
Simon V. 2 days ago
committed by GitHub
parent
commit
731f47cc90
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      .github/workflows/wiki-monitor.yml
  2. 8
      Dockerfile
  3. 55
      README.md
  4. 43
      acme.sh
  5. 23
      deploy/panos.sh
  6. 17
      dnsapi/dns_ali.sh
  7. 2
      dnsapi/dns_aws.sh
  8. 4
      dnsapi/dns_cf.sh
  9. 593
      dnsapi/dns_hetznercloud.sh
  10. 244
      dnsapi/dns_infoblox_uddi.sh

5
.github/workflows/wiki-monitor.yml

@ -22,6 +22,7 @@ jobs:
page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH") page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH")
page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH") page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH")
page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH") page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH")
page_summary=$(jq -r '.pages[0].summary' "$GITHUB_EVENT_PATH")
now="$(date '+%Y-%m-%d %H:%M:%S')" now="$(date '+%Y-%m-%d %H:%M:%S')"
cd wiki cd wiki
@ -35,9 +36,11 @@ jobs:
{ {
echo "Wiki edited" echo "Wiki edited"
echo -n "User: " echo -n "User: "
echo "[$actor]($sender_url)"
echo "@$actor [$actor]($sender_url)"
echo "Time: $now" echo "Time: $now"
echo "Page: [$page_name]($page_url) (Action: $page_action)" echo "Page: [$page_name]($page_url) (Action: $page_action)"
echo "Comment: $page_summary"
echo "[Click here to Revert](${page_url}/_history)"
echo "" echo ""
echo "----" echo "----"
echo "### diff:" echo "### diff:"

8
Dockerfile

@ -15,6 +15,8 @@ RUN apk --no-cache add -f \
jq \ jq \
cronie cronie
ENV LE_WORKING_DIR=/acmebin
ENV LE_CONFIG_HOME=/acme.sh ENV LE_CONFIG_HOME=/acme.sh
ARG AUTO_UPGRADE=1 ARG AUTO_UPGRADE=1
@ -30,7 +32,7 @@ COPY ./notify /install_acme.sh/notify
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/ 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/
RUN ln -s /root/.acme.sh/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab -
RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab -
RUN for verb in help \ RUN for verb in help \
version \ version \
@ -64,7 +66,7 @@ RUN for verb in help \
set-default-ca \ set-default-ca \
set-default-chain \ set-default-chain \
; do \ ; 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} \
printf -- "%b" "#!/usr/bin/env sh\n$LE_WORKING_DIR/acme.sh --${verb} --config-home $LE_CONFIG_HOME \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \
; done ; done
RUN printf "%b" '#!'"/usr/bin/env sh\n \ RUN printf "%b" '#!'"/usr/bin/env sh\n \
@ -72,7 +74,7 @@ if [ \"\$1\" = \"daemon\" ]; then \n \
exec crond -n -s -m off \n \ exec crond -n -s -m off \n \
else \n \ else \n \
exec -- \"\$@\"\n \ exec -- \"\$@\"\n \
fi\n" >/entry.sh && chmod +x /entry.sh
fi\n" >/entry.sh && chmod +x /entry.sh && chmod -R o+rwx $LE_WORKING_DIR && chmod -R o+rwx $LE_CONFIG_HOME
VOLUME /acme.sh VOLUME /acme.sh

55
README.md

@ -1,3 +1,5 @@
[![zerossl.com](https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05)](https://zerossl.com/?fromacme.sh)
# An ACME Shell script: 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) [![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)
@ -84,7 +86,6 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|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)|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)|Oracle Linux
|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)|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)|Mageia
|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)|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)|Gentoo Linux
|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)|ClearLinux
|22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 |22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111
|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) |23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
|24|[![](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) |24|[![](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)
@ -207,6 +208,8 @@ The certs will be placed in `~/.acme.sh/example.com/`
The certs will be renewed automatically every **60** days. The certs will be renewed automatically every **60** days.
The certs will default to ECC certificates.
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
@ -358,36 +361,33 @@ Ok, it's done.
**Please use dns api mode instead.** **Please use dns api mode instead.**
# 10. Issue ECC certificates
# 10. Issue certificates of different key types and lengths (ECC or RSA)
Just set the `keylength` to a valid, supported, value.
Just set the `keylength` parameter with a prefix `ec-`.
Valid values for the `keylength` parameter are:
1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)**
2. **ec-384 (secp384r1, "ECDSA P-384")**
3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)**
4. **2048 (RSA2048)**
5. **3072 (RSA3072)**
6. **4096 (RSA4096)**
For example: For example:
### Single domain ECC certificate
### Single domain with ECDSA P-384 certificate
```bash ```bash
acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-256
acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-384
``` ```
### SAN multi domain ECC certificate
### SAN multi domain with RSA4096 certificate
```bash ```bash
acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength ec-256
acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096
``` ```
Please look at the `keylength` parameter above.
Valid values are:
1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)**
2. **ec-384 (secp384r1, "ECDSA P-384")**
3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)**
4. **2048 (RSA2048)**
5. **3072 (RSA3072)**
6. **4096 (RSA4096)**
# 11. Issue Wildcard certificates # 11. Issue Wildcard certificates
It's simple, just give a wildcard domain as the `-d` parameter. It's simple, just give a wildcard domain as the `-d` parameter.
@ -523,3 +523,20 @@ Your donation makes **acme.sh** better:
1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/) 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)
[Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list) [Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)
# 21. About this repository
> [!NOTE]
> This repository is officially maintained by <strong>ZeroSSL</strong> as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community!
> For more information about our services, including free and paid SSL/TLS certificates, visit https://zerossl.com.
>
> All donations made through this repository go directly to the original independent maintainer (Neil Pang), not to ZeroSSL.
<p align="center">
<a href="https://zerossl.com.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://zerossl.com/assets/images/zerossl_logo_white.svg">
<source media="(prefers-color-scheme: light)" srcset="https://zerossl.com/assets/images/zerossl_logo.svg">
<img src="https://zerossl.com/assets/images/zerossl_logo.svg" alt="ZeroSSL" width="256">
</picture>
</a>
</p>

43
acme.sh

@ -1,6 +1,6 @@
#!/usr/bin/env sh #!/usr/bin/env sh
VER=3.1.2
VER=3.1.3
PROJECT_NAME="acme.sh" PROJECT_NAME="acme.sh"
@ -1250,7 +1250,7 @@ _idn() {
fi fi
} }
#_createcsr cn san_list keyfile csrfile conf acmeValidationv1
#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 extendedUsage
_createcsr() { _createcsr() {
_debug _createcsr _debug _createcsr
domain="$1" domain="$1"
@ -1259,6 +1259,7 @@ _createcsr() {
csr="$4" csr="$4"
csrconf="$5" csrconf="$5"
acmeValidationv1="$6" acmeValidationv1="$6"
extusage="$7"
_debug2 domain "$domain" _debug2 domain "$domain"
_debug2 domainlist "$domainlist" _debug2 domainlist "$domainlist"
_debug2 csrkey "$csrkey" _debug2 csrkey "$csrkey"
@ -1267,9 +1268,8 @@ _createcsr() {
printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]" >"$csrconf" printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]" >"$csrconf"
if [ "$Le_ExtKeyUse" ]; then
_savedomainconf Le_ExtKeyUse "$Le_ExtKeyUse"
printf "\nextendedKeyUsage=$Le_ExtKeyUse\n" >>"$csrconf"
if [ "$extusage" ]; then
printf "\nextendedKeyUsage=$extusage\n" >>"$csrconf"
else else
printf "\nextendedKeyUsage=serverAuth,clientAuth\n" >>"$csrconf" printf "\nextendedKeyUsage=serverAuth,clientAuth\n" >>"$csrconf"
fi fi
@ -4511,6 +4511,7 @@ issue() {
_valid_from="${16}" _valid_from="${16}"
_valid_to="${17}" _valid_to="${17}"
_certificate_profile="${18}" _certificate_profile="${18}"
_extended_key_usage="${19}"
if [ -z "$_ACME_IS_RENEW" ]; then if [ -z "$_ACME_IS_RENEW" ]; then
_initpath "$_main_domain" "$_key_length" _initpath "$_main_domain" "$_key_length"
@ -4655,12 +4656,25 @@ issue() {
return 1 return 1
fi fi
fi fi
if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then
_keyusage="$_extended_key_usage"
if [ "$Le_API" = "$CA_GOOGLE" ] || [ "$Le_API" = "$CA_GOOGLE_TEST" ]; then
if [ -z "$_keyusage" ]; then
#https://github.com/acmesh-official/acme.sh/issues/6610
#google accepts serverauth only
_keyusage="serverAuth"
fi
fi
if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_keyusage"; then
_err "Error creating CSR." _err "Error creating CSR."
_clearup _clearup
_on_issue_err "$_post_hook" _on_issue_err "$_post_hook"
return 1 return 1
fi fi
if [ "$_extended_key_usage" ]; then
_savedomainconf "Le_ExtKeyUse" "$_extended_key_usage"
else
_cleardomainconf "Le_ExtKeyUse"
fi
fi fi
_savedomainconf "Le_Keylength" "$_key_length" _savedomainconf "Le_Keylength" "$_key_length"
@ -5298,6 +5312,16 @@ $_authorizations_map"
return 1 return 1
fi fi
break break
elif _contains "$response" "\"ready\""; then
_info "Order status is 'ready', let's sleep and retry."
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
_debug "_retryafter" "$_retryafter"
if [ "$_retryafter" ]; then
_info "Sleeping for $_retryafter seconds then retrying"
_sleep $_retryafter
else
_sleep 2
fi
elif _contains "$response" "\"processing\""; then elif _contains "$response" "\"processing\""; then
_info "Order status is 'processing', let's sleep and retry." _info "Order status is 'processing', let's sleep and retry."
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
@ -5650,7 +5674,7 @@ renew() {
_cleardomainconf Le_OCSP_Staple _cleardomainconf Le_OCSP_Staple
fi fi
fi fi
issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile"
issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" "$Le_ExtKeyUse"
res="$?" res="$?"
if [ "$res" != "0" ]; then if [ "$res" != "0" ]; then
return "$res" return "$res"
@ -7570,6 +7594,7 @@ _process() {
_valid_from="" _valid_from=""
_valid_to="" _valid_to=""
_certificate_profile="" _certificate_profile=""
_extended_key_usage=""
while [ ${#} -gt 0 ]; do while [ ${#} -gt 0 ]; do
case "${1}" in case "${1}" in
@ -7974,7 +7999,7 @@ _process() {
shift shift
;; ;;
--extended-key-usage) --extended-key-usage)
Le_ExtKeyUse="$2"
_extended_key_usage="$2"
shift shift
;; ;;
--ocsp-must-staple | --ocsp) --ocsp-must-staple | --ocsp)
@ -8191,7 +8216,7 @@ _process() {
uninstall) uninstall "$_nocron" ;; uninstall) uninstall "$_nocron" ;;
upgrade) upgrade ;; upgrade) upgrade ;;
issue) issue)
issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile"
issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage"
;; ;;
deploy) deploy)
deploy "$_domain" "$_deploy_hook" "$_ecc" deploy "$_domain" "$_deploy_hook" "$_ecc"

23
deploy/panos.sh

@ -16,6 +16,7 @@
# export PANOS_TEMPLATE="" # Template Name of panorama managed devices # export PANOS_TEMPLATE="" # Template Name of panorama managed devices
# export PANOS_TEMPLATE_STACK="" # set a Template Stack if certificate should also be pushed automatically # export PANOS_TEMPLATE_STACK="" # set a Template Stack if certificate should also be pushed automatically
# export PANOS_VSYS="Shared" # name of the vsys to import the certificate # export PANOS_VSYS="Shared" # name of the vsys to import the certificate
# export PANOS_CERTNAME="" # use a custom certificate name to work around Panorama's 31-character limit
# #
# The script will automatically generate a new API key if # The script will automatically generate a new API key if
# no key is found, or if a saved key has expired or is invalid. # no key is found, or if a saved key has expired or is invalid.
@ -89,7 +90,7 @@ deployer() {
if [ "$type" = 'cert' ]; then if [ "$type" = 'cert' ]; then
panos_url="${panos_url}?type=import" panos_url="${panos_url}?type=import"
content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\ncertificate" content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\ncertificate"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")"
@ -103,11 +104,11 @@ deployer() {
if [ "$type" = 'key' ]; then if [ "$type" = 'key' ]; then
panos_url="${panos_url}?type=import" panos_url="${panos_url}?type=import"
content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\nprivate-key" content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\nprivate-key"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"passphrase\"\r\n\r\n123456" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"passphrase\"\r\n\r\n123456"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cdomain.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_panos_certname.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
if [ "$_panos_template" ]; then if [ "$_panos_template" ]; then
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template"
fi fi
@ -242,6 +243,15 @@ panos_deploy() {
_getdeployconf PANOS_VSYS _getdeployconf PANOS_VSYS
fi fi
# PANOS_CERTNAME
if [ "$PANOS_CERTNAME" ]; then
_debug "Detected ENV variable PANOS_CERTNAME. Saving to file."
_savedeployconf PANOS_CERTNAME "$PANOS_CERTNAME" 1
else
_debug "Attempting to load variable PANOS_CERTNAME from file."
_getdeployconf PANOS_CERTNAME
fi
#Store variables #Store variables
_panos_host=$PANOS_HOST _panos_host=$PANOS_HOST
_panos_user=$PANOS_USER _panos_user=$PANOS_USER
@ -249,6 +259,7 @@ panos_deploy() {
_panos_template=$PANOS_TEMPLATE _panos_template=$PANOS_TEMPLATE
_panos_template_stack=$PANOS_TEMPLATE_STACK _panos_template_stack=$PANOS_TEMPLATE_STACK
_panos_vsys=$PANOS_VSYS _panos_vsys=$PANOS_VSYS
_panos_certname=$PANOS_CERTNAME
#Test API Key if found. If the key is invalid, the variable _panos_key will be unset. #Test API Key if found. If the key is invalid, the variable _panos_key will be unset.
if [ "$_panos_host" ] && [ "$_panos_key" ]; then if [ "$_panos_host" ] && [ "$_panos_key" ]; then
@ -267,6 +278,12 @@ panos_deploy() {
_err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs." _err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs."
return 1 return 1
else else
# Use certificate name based on the first domain on the certificate if no custom certificate name is set
if [ -z "$_panos_certname" ]; then
_panos_certname="$_cdomain"
_savedeployconf PANOS_CERTNAME "$_panos_certname" 1
fi
# Generate a new API key if no valid API key is found # Generate a new API key if no valid API key is found
if [ -z "$_panos_key" ]; then if [ -z "$_panos_key" ]; then
_debug "**** Generating new PANOS API KEY ****" _debug "**** Generating new PANOS API KEY ****"

17
dnsapi/dns_ali.sh

@ -97,12 +97,13 @@ _ali_rest() {
} }
_ali_nonce() { _ali_nonce() {
#_head_n 1 </dev/urandom | _digest "sha256" hex | cut -c 1-31
#Not so good...
date +"%s%N" | sed 's/%N//g'
if [ "$ACME_OPENSSL_BIN" ]; then
"$ACME_OPENSSL_BIN" rand -hex 16 2>/dev/null && return 0
fi
printf "%s" "$(date +%s)$$$(date +%N)" | _digest sha256 hex | cut -c 1-32
} }
_timestamp() {
_ali_timestamp() {
date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ" date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ"
} }
@ -150,7 +151,7 @@ _check_exist_query() {
query=$query'&SignatureMethod=HMAC-SHA1' query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)" query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0' query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&TypeKeyWord=TXT' query=$query'&TypeKeyWord=TXT'
query=$query'&Version=2015-01-09' query=$query'&Version=2015-01-09'
} }
@ -166,7 +167,7 @@ _add_record_query() {
query=$query'&SignatureMethod=HMAC-SHA1' query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)" query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0' query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Type=TXT' query=$query'&Type=TXT'
query=$query'&Value='$3 query=$query'&Value='$3
query=$query'&Version=2015-01-09' query=$query'&Version=2015-01-09'
@ -182,7 +183,7 @@ _delete_record_query() {
query=$query'&SignatureMethod=HMAC-SHA1' query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)" query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0' query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2015-01-09' query=$query'&Version=2015-01-09'
} }
@ -196,7 +197,7 @@ _describe_records_query() {
query=$query'&SignatureMethod=HMAC-SHA1' query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)" query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0' query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2015-01-09' query=$query'&Version=2015-01-09'
} }

2
dnsapi/dns_aws.sh

@ -161,7 +161,7 @@ _get_root() {
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100 | sed 's/\./\\./g') h=$(printf "%s" "$domain" | cut -d . -f "$i"-100 | sed 's/\./\\./g')
_debug "Checking domain: $h" _debug "Checking domain: $h"
if [ -z "$h" ]; then if [ -z "$h" ]; then
_error "invalid domain"
_err "invalid domain"
return 1 return 1
fi fi

4
dnsapi/dns_cf.sh

@ -92,7 +92,9 @@ dns_cf_add() {
if _contains "$response" "$txtvalue"; then if _contains "$response" "$txtvalue"; then
_info "Added, OK" _info "Added, OK"
return 0 return 0
elif _contains "$response" "The record already exists"; then
elif _contains "$response" "The record already exists" ||
_contains "$response" "An identical record already exists." ||
_contains "$response" '"code":81058'; then
_info "Already exists, OK" _info "Already exists, OK"
return 0 return 0
else else

593
dnsapi/dns_hetznercloud.sh

@ -0,0 +1,593 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_hetznercloud_info='Hetzner Cloud DNS
Site: Hetzner.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hetznercloud
Options:
HETZNER_TOKEN API token for the Hetzner Cloud DNS API
Optional:
HETZNER_TTL Custom TTL for new TXT rrsets (default 120)
HETZNER_API Override API endpoint (default https://api.hetzner.cloud/v1)
HETZNER_MAX_ATTEMPTS Number of 1s polls to wait for async actions (default 120)
Issues: github.com/acmesh-official/acme.sh/issues
'
HETZNERCLOUD_API_DEFAULT="https://api.hetzner.cloud/v1"
HETZNERCLOUD_TTL_DEFAULT=120
HETZNER_MAX_ATTEMPTS_DEFAULT=120
######## Public functions #####################
dns_hetznercloud_add() {
fulldomain="$(_idn "${1}")"
txtvalue="${2}"
_info "Using Hetzner Cloud DNS API to add record"
if ! _hetznercloud_init; then
return 1
fi
if ! _hetznercloud_prepare_zone "${fulldomain}"; then
_err "Unable to determine Hetzner Cloud zone for ${fulldomain}"
return 1
fi
if ! _hetznercloud_get_rrset; then
return 1
fi
if [ "${_hetznercloud_last_http_code}" = "200" ]; then
if _hetznercloud_rrset_contains_value "${txtvalue}"; then
_info "TXT record already present; nothing to do."
return 0
fi
elif [ "${_hetznercloud_last_http_code}" != "404" ]; then
_hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}"
return 1
fi
add_payload="$(_hetznercloud_build_add_payload "${txtvalue}")"
if [ -z "${add_payload}" ]; then
_err "Failed to build request payload."
return 1
fi
if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_add}" "${add_payload}"; then
return 1
fi
case "${_hetznercloud_last_http_code}" in
200 | 201 | 202 | 204)
if ! _hetznercloud_handle_action_response "TXT record add"; then
return 1
fi
_info "Hetzner Cloud TXT record added."
return 0
;;
401 | 403)
_err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API."
_hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}"
return 1
;;
409 | 422)
_hetznercloud_log_http_error "Hetzner Cloud DNS rejected the add_records request" "${_hetznercloud_last_http_code}"
return 1
;;
*)
_hetznercloud_log_http_error "Hetzner Cloud DNS add_records request failed" "${_hetznercloud_last_http_code}"
return 1
;;
esac
}
dns_hetznercloud_rm() {
fulldomain="$(_idn "${1}")"
txtvalue="${2}"
_info "Using Hetzner Cloud DNS API to remove record"
if ! _hetznercloud_init; then
return 1
fi
if ! _hetznercloud_prepare_zone "${fulldomain}"; then
_err "Unable to determine Hetzner Cloud zone for ${fulldomain}"
return 1
fi
if ! _hetznercloud_get_rrset; then
return 1
fi
if [ "${_hetznercloud_last_http_code}" = "404" ]; then
_info "TXT rrset does not exist; nothing to remove."
return 0
fi
if [ "${_hetznercloud_last_http_code}" != "200" ]; then
_hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}"
return 1
fi
if _hetznercloud_rrset_contains_value "${txtvalue}"; then
remove_payload="$(_hetznercloud_build_remove_payload "${txtvalue}")"
if [ -z "${remove_payload}" ]; then
_err "Failed to build remove_records payload."
return 1
fi
if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_remove}" "${remove_payload}"; then
return 1
fi
case "${_hetznercloud_last_http_code}" in
200 | 201 | 202 | 204)
if ! _hetznercloud_handle_action_response "TXT record remove"; then
return 1
fi
_info "Hetzner Cloud TXT record removed."
return 0
;;
401 | 403)
_err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API."
_hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}"
return 1
;;
404)
_info "TXT rrset already absent after remove action."
return 0
;;
409 | 422)
_hetznercloud_log_http_error "Hetzner Cloud DNS rejected the remove_records request" "${_hetznercloud_last_http_code}"
return 1
;;
*)
_hetznercloud_log_http_error "Hetzner Cloud DNS remove_records request failed" "${_hetznercloud_last_http_code}"
return 1
;;
esac
else
_info "TXT value not present; nothing to remove."
return 0
fi
}
#################### Private functions ##################################
_hetznercloud_init() {
HETZNER_TOKEN="${HETZNER_TOKEN:-$(_readaccountconf_mutable HETZNER_TOKEN)}"
if [ -z "${HETZNER_TOKEN}" ]; then
_err "The environment variable HETZNER_TOKEN must be set for the Hetzner Cloud DNS API."
return 1
fi
HETZNER_TOKEN=$(echo "${HETZNER_TOKEN}" | tr -d '"')
_saveaccountconf_mutable HETZNER_TOKEN "${HETZNER_TOKEN}"
HETZNER_API="${HETZNER_API:-$(_readaccountconf_mutable HETZNER_API)}"
if [ -z "${HETZNER_API}" ]; then
HETZNER_API="${HETZNERCLOUD_API_DEFAULT}"
fi
_saveaccountconf_mutable HETZNER_API "${HETZNER_API}"
HETZNER_TTL="${HETZNER_TTL:-$(_readaccountconf_mutable HETZNER_TTL)}"
if [ -z "${HETZNER_TTL}" ]; then
HETZNER_TTL="${HETZNERCLOUD_TTL_DEFAULT}"
fi
ttl_check=$(printf "%s" "${HETZNER_TTL}" | tr -d '0-9')
if [ -n "${ttl_check}" ]; then
_err "HETZNER_TTL must be an integer value."
return 1
fi
_saveaccountconf_mutable HETZNER_TTL "${HETZNER_TTL}"
HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS:-$(_readaccountconf_mutable HETZNER_MAX_ATTEMPTS)}"
if [ -z "${HETZNER_MAX_ATTEMPTS}" ]; then
HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS_DEFAULT}"
fi
attempts_check=$(printf "%s" "${HETZNER_MAX_ATTEMPTS}" | tr -d '0-9')
if [ -n "${attempts_check}" ]; then
_err "HETZNER_MAX_ATTEMPTS must be an integer value."
return 1
fi
_saveaccountconf_mutable HETZNER_MAX_ATTEMPTS "${HETZNER_MAX_ATTEMPTS}"
return 0
}
_hetznercloud_prepare_zone() {
_hetznercloud_zone_id=""
_hetznercloud_zone_name=""
_hetznercloud_zone_name_lc=""
_hetznercloud_rr_name=""
_hetznercloud_rrset_path=""
_hetznercloud_rrset_action_add=""
_hetznercloud_rrset_action_remove=""
fulldomain_lc=$(printf "%s" "${1}" | sed 's/\.$//' | _lower_case)
i=2
p=1
while true; do
candidate=$(printf "%s" "${fulldomain_lc}" | cut -d . -f "${i}"-100)
if [ -z "${candidate}" ]; then
return 1
fi
if _hetznercloud_get_zone_by_candidate "${candidate}"; then
zone_name_lc="${_hetznercloud_zone_name_lc}"
if [ "${fulldomain_lc}" = "${zone_name_lc}" ]; then
_hetznercloud_rr_name="@"
else
suffix=".${zone_name_lc}"
if _endswith "${fulldomain_lc}" "${suffix}"; then
_hetznercloud_rr_name="${fulldomain_lc%"${suffix}"}"
else
_hetznercloud_rr_name="${fulldomain_lc}"
fi
fi
_hetznercloud_rrset_path=$(printf "%s" "${_hetznercloud_rr_name}" | _url_encode)
_hetznercloud_rrset_action_add="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/add_records"
_hetznercloud_rrset_action_remove="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/remove_records"
return 0
fi
p=${i}
i=$(_math "${i}" + 1)
done
}
_hetznercloud_get_zone_by_candidate() {
candidate="${1}"
zone_key=$(printf "%s" "${candidate}" | sed 's/[^A-Za-z0-9]/_/g')
zone_conf_key="HETZNERCLOUD_ZONE_ID_for_${zone_key}"
cached_zone_id=$(_readdomainconf "${zone_conf_key}")
if [ -n "${cached_zone_id}" ]; then
if _hetznercloud_api GET "/zones/${cached_zone_id}"; then
if [ "${_hetznercloud_last_http_code}" = "200" ]; then
zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//')
if _hetznercloud_parse_zone_fields "${zone_data}"; then
zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case)
if [ "${zone_name_lc}" = "${candidate}" ]; then
return 0
fi
fi
elif [ "${_hetznercloud_last_http_code}" = "404" ]; then
_cleardomainconf "${zone_conf_key}"
fi
else
return 1
fi
fi
if _hetznercloud_api GET "/zones/${candidate}"; then
if [ "${_hetznercloud_last_http_code}" = "200" ]; then
zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//')
if _hetznercloud_parse_zone_fields "${zone_data}"; then
zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case)
if [ "${zone_name_lc}" = "${candidate}" ]; then
_savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}"
return 0
fi
fi
elif [ "${_hetznercloud_last_http_code}" != "404" ]; then
_hetznercloud_log_http_error "Hetzner Cloud zone lookup failed" "${_hetznercloud_last_http_code}"
return 1
fi
else
return 1
fi
encoded_candidate=$(printf "%s" "${candidate}" | _url_encode)
if ! _hetznercloud_api GET "/zones?name=${encoded_candidate}"; then
return 1
fi
if [ "${_hetznercloud_last_http_code}" != "200" ]; then
if [ "${_hetznercloud_last_http_code}" = "404" ]; then
return 1
fi
_hetznercloud_log_http_error "Hetzner Cloud zone search failed" "${_hetznercloud_last_http_code}"
return 1
fi
zone_data=$(_hetznercloud_extract_zone_from_list "${response}" "${candidate}")
if [ -z "${zone_data}" ]; then
return 1
fi
if ! _hetznercloud_parse_zone_fields "${zone_data}"; then
return 1
fi
_savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}"
return 0
}
_hetznercloud_parse_zone_fields() {
zone_json="${1}"
if [ -z "${zone_json}" ]; then
return 1
fi
normalized=$(printf "%s" "${zone_json}" | _normalizeJson)
zone_id=$(printf "%s" "${normalized}" | _egrep_o '"id":[^,}]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')
zone_name=$(printf "%s" "${normalized}" | _egrep_o '"name":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
if [ -z "${zone_id}" ] || [ -z "${zone_name}" ]; then
return 1
fi
zone_name_trimmed=$(printf "%s" "${zone_name}" | sed 's/\.$//')
if zone_name_ascii=$(_idn "${zone_name_trimmed}"); then
zone_name="${zone_name_ascii}"
else
zone_name="${zone_name_trimmed}"
fi
_hetznercloud_zone_id="${zone_id}"
_hetznercloud_zone_name="${zone_name}"
_hetznercloud_zone_name_lc=$(printf "%s" "${zone_name}" | _lower_case)
return 0
}
_hetznercloud_extract_zone_from_list() {
list_response=$(printf "%s" "${1}" | _normalizeJson)
candidate="${2}"
escaped_candidate=$(_hetznercloud_escape_regex "${candidate}")
printf "%s" "${list_response}" | _egrep_o "{[^{}]*\"name\":\"${escaped_candidate}\"[^{}]*}" | _head_n 1
}
_hetznercloud_escape_regex() {
printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/\./\\./g' | sed 's/-/\\-/g'
}
_hetznercloud_get_rrset() {
if [ -z "${_hetznercloud_zone_id}" ] || [ -z "${_hetznercloud_rrset_path}" ]; then
return 1
fi
if ! _hetznercloud_api GET "/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT"; then
return 1
fi
return 0
}
_hetznercloud_rrset_contains_value() {
wanted_value="${1}"
normalized=$(printf "%s" "${response}" | _normalizeJson)
escaped_value=$(_hetznercloud_escape_value "${wanted_value}")
search_pattern="\"value\":\"\\\\\"${escaped_value}\\\\\"\""
if _contains "${normalized}" "${search_pattern}"; then
return 0
fi
return 1
}
_hetznercloud_build_add_payload() {
value="${1}"
escaped_value=$(_hetznercloud_escape_value "${value}")
printf '{"ttl":%s,"records":[{"value":"\\"%s\\""}]}' "${HETZNER_TTL}" "${escaped_value}"
}
_hetznercloud_build_remove_payload() {
value="${1}"
escaped_value=$(_hetznercloud_escape_value "${value}")
printf '{"records":[{"value":"\\"%s\\""}]}' "${escaped_value}"
}
_hetznercloud_escape_value() {
printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g'
}
_hetznercloud_error_message() {
if [ -z "${response}" ]; then
return 1
fi
message=$(printf "%s" "${response}" | _normalizeJson | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
if [ -n "${message}" ]; then
printf "%s" "${message}"
return 0
fi
return 1
}
_hetznercloud_log_http_error() {
context="${1}"
code="${2}"
message="$(_hetznercloud_error_message)"
if [ -n "${context}" ]; then
if [ -n "${message}" ]; then
_err "${context} (HTTP ${code}): ${message}"
else
_err "${context} (HTTP ${code})"
fi
else
if [ -n "${message}" ]; then
_err "Hetzner Cloud DNS API error (HTTP ${code}): ${message}"
else
_err "Hetzner Cloud DNS API error (HTTP ${code})"
fi
fi
}
_hetznercloud_api() {
method="${1}"
ep="${2}"
data="${3}"
retried="${4}"
if [ -z "${method}" ]; then
method="GET"
fi
if ! _startswith "${ep}" "/"; then
ep="/${ep}"
fi
url="${HETZNER_API}${ep}"
export _H1="Authorization: Bearer ${HETZNER_TOKEN}"
export _H2="Accept: application/json"
export _H3=""
export _H4=""
export _H5=""
: >"${HTTP_HEADER}"
if [ "${method}" = "GET" ]; then
response="$(_get "${url}")"
else
if [ -z "${data}" ]; then
data="{}"
fi
response="$(_post "${data}" "${url}" "" "${method}" "application/json")"
fi
ret="${?}"
_hetznercloud_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
if [ "${ret}" != "0" ]; then
return 1
fi
if [ "${_hetznercloud_last_http_code}" = "429" ] && [ "${retried}" != "retried" ]; then
retry_after=$(grep -i "^Retry-After" "${HTTP_HEADER}" | _tail_n 1 | cut -d : -f 2 | tr -d ' \r')
if [ -z "${retry_after}" ]; then
retry_after=1
fi
_info "Hetzner Cloud DNS API rate limit hit; retrying in ${retry_after} seconds."
_sleep "${retry_after}"
if ! _hetznercloud_api "${method}" "${ep}" "${data}" "retried"; then
return 1
fi
return 0
fi
return 0
}
_hetznercloud_handle_action_response() {
context="${1}"
if [ -z "${response}" ]; then
return 0
fi
normalized=$(printf "%s" "${response}" | _normalizeJson)
failed_message=""
if failed_message=$(_hetznercloud_extract_failed_action_message "${normalized}"); then
if [ -n "${failed_message}" ]; then
_err "Hetzner Cloud DNS ${context} failed: ${failed_message}"
else
_err "Hetzner Cloud DNS ${context} failed."
fi
return 1
fi
action_ids=""
if action_ids=$(_hetznercloud_extract_action_ids "${normalized}"); then
for action_id in ${action_ids}; do
if [ -z "${action_id}" ]; then
continue
fi
if ! _hetznercloud_wait_for_action "${action_id}" "${context}"; then
return 1
fi
done
fi
return 0
}
_hetznercloud_extract_failed_action_message() {
normalized="${1}"
failed_section=$(printf "%s" "${normalized}" | _egrep_o '"failed_actions":\[[^]]*\]')
if [ -z "${failed_section}" ]; then
return 1
fi
if _contains "${failed_section}" '"failed_actions":[]'; then
return 1
fi
message=$(printf "%s" "${failed_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
if [ -n "${message}" ]; then
printf "%s" "${message}"
else
printf "%s" "${failed_section}"
fi
return 0
}
_hetznercloud_extract_action_ids() {
normalized="${1}"
actions_section=$(printf "%s" "${normalized}" | _egrep_o '"actions":\[[^]]*\]')
if [ -z "${actions_section}" ]; then
return 1
fi
action_ids=$(printf "%s" "${actions_section}" | _egrep_o '"id":[0-9]*' | cut -d : -f 2 | tr -d '"' | tr '\n' ' ')
action_ids=$(printf "%s" "${action_ids}" | tr -s ' ')
action_ids=$(printf "%s" "${action_ids}" | sed 's/^ //;s/ $//')
if [ -z "${action_ids}" ]; then
return 1
fi
printf "%s" "${action_ids}"
return 0
}
_hetznercloud_wait_for_action() {
action_id="${1}"
context="${2}"
attempts="0"
while true; do
if ! _hetznercloud_api GET "/actions/${action_id}"; then
return 1
fi
if [ "${_hetznercloud_last_http_code}" != "200" ]; then
_hetznercloud_log_http_error "Hetzner Cloud DNS action ${action_id} query failed" "${_hetznercloud_last_http_code}"
return 1
fi
normalized=$(printf "%s" "${response}" | _normalizeJson)
action_status=$(_hetznercloud_action_status_from_normalized "${normalized}")
if [ -z "${action_status}" ]; then
_err "Hetzner Cloud DNS ${context} action ${action_id} returned no status."
return 1
fi
if [ "${action_status}" = "success" ]; then
return 0
fi
if [ "${action_status}" = "error" ]; then
if action_error=$(_hetznercloud_action_error_from_normalized "${normalized}"); then
_err "Hetzner Cloud DNS ${context} action ${action_id} failed: ${action_error}"
else
_err "Hetzner Cloud DNS ${context} action ${action_id} failed."
fi
return 1
fi
attempts=$(_math "${attempts}" + 1)
if [ "${attempts}" -ge "${HETZNER_MAX_ATTEMPTS}" ]; then
_err "Hetzner Cloud DNS ${context} action ${action_id} did not complete after ${HETZNER_MAX_ATTEMPTS} attempts."
return 1
fi
_sleep 1
done
}
_hetznercloud_action_status_from_normalized() {
normalized="${1}"
status=$(printf "%s" "${normalized}" | _egrep_o '"status":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
printf "%s" "${status}"
}
_hetznercloud_action_error_from_normalized() {
normalized="${1}"
error_section=$(printf "%s" "${normalized}" | _egrep_o '"error":{[^}]*}')
if [ -z "${error_section}" ]; then
return 1
fi
message=$(printf "%s" "${error_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
if [ -n "${message}" ]; then
printf "%s" "${message}"
return 0
fi
code=$(printf "%s" "${error_section}" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
if [ -n "${code}" ]; then
printf "%s" "${code}"
return 0
fi
return 1
}

244
dnsapi/dns_infoblox_uddi.sh

@ -0,0 +1,244 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_infoblox_uddi_info='Infoblox UDDI
Site: Infoblox.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_infoblox_uddi
Options:
Infoblox_UDDI_Key API Key for Infoblox UDDI
Infoblox_Portal URL, e.g. "csp.infoblox.com" or "csp.eu.infoblox.com"
Issues: github.com/acmesh-official/acme.sh/issues
Author: Stefan Riegel
'
Infoblox_UDDI_Api="https://"
######## Public functions #####################
#Usage: dns_infoblox_uddi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_infoblox_uddi_add() {
fulldomain=$1
txtvalue=$2
Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}"
Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}"
_info "Using Infoblox UDDI API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if [ -z "$Infoblox_UDDI_Key" ] || [ -z "$Infoblox_Portal" ]; then
Infoblox_UDDI_Key=""
Infoblox_Portal=""
_err "You didn't specify the Infoblox UDDI key or server (Infoblox_UDDI_Key; Infoblox_Portal)."
_err "Please set them via EXPORT Infoblox_UDDI_Key=your_key, EXPORT Infoblox_Portal=csp.infoblox.com and try again."
return 1
fi
_saveaccountconf_mutable Infoblox_UDDI_Key "$Infoblox_UDDI_Key"
_saveaccountconf_mutable Infoblox_Portal "$Infoblox_Portal"
export _H1="Authorization: Token $Infoblox_UDDI_Key"
export _H2="Content-Type: application/json"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug "Getting existing txt records"
_infoblox_rest GET "dns/record?_filter=type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'"
_info "Adding record"
body="{\"type\":\"TXT\",\"name_in_zone\":\"$_sub_domain\",\"zone\":\"$_domain_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}"
if _infoblox_rest POST "dns/record" "$body"; then
if _contains "$response" "$txtvalue"; then
_info "Added, OK"
return 0
elif _contains "$response" '"error"'; then
# Check if record already exists
if _contains "$response" "already exists" || _contains "$response" "duplicate"; then
_info "Already exists, OK"
return 0
else
_err "Add txt record error."
_err "Response: $response"
return 1
fi
else
_info "Added, OK"
return 0
fi
fi
_err "Add txt record error."
return 1
}
#Usage: dns_infoblox_uddi_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_infoblox_uddi_rm() {
fulldomain=$1
txtvalue=$2
Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}"
Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}"
if [ -z "$Infoblox_UDDI_Key" ] || [ -z "$Infoblox_Portal" ]; then
_err "Credentials not found"
return 1
fi
_info "Using Infoblox UDDI API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
export _H1="Authorization: Token $Infoblox_UDDI_Key"
export _H2="Content-Type: application/json"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug "Getting txt records to delete"
# Filter by txtvalue to support wildcard certs (multiple TXT records)
filter="type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'%20and%20rdata.text%20eq%20'$txtvalue'"
_infoblox_rest GET "dns/record?_filter=$filter"
if ! _contains "$response" '"results"'; then
_info "Don't need to remove, record not found."
return 0
fi
record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_debug "record_id" "$record_id"
if [ -z "$record_id" ]; then
_info "Don't need to remove, record not found."
return 0
fi
# Extract UUID from the full record ID (format: dns/record/uuid)
record_uuid=$(echo "$record_id" | sed 's|.*/||')
_debug "record_uuid" "$record_uuid"
if ! _infoblox_rest DELETE "dns/record/$record_uuid"; then
_err "Delete record error."
return 1
fi
_info "Removed record successfully"
return 0
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_id=dns/auth_zone/xxxx-xxxx
_get_root() {
domain=$1
i=1
p=1
# Remove _acme-challenge prefix if present
domain_no_acme=$(echo "$domain" | sed 's/^_acme-challenge\.//')
while true; do
h=$(printf "%s" "$domain_no_acme" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
# not valid
return 1
fi
# Query for the zone with both trailing dot and without
filter="fqdn%20eq%20'$h.'%20or%20fqdn%20eq%20'$h'"
if ! _infoblox_rest GET "dns/auth_zone?_filter=$filter"; then
# API error - don't continue if we get auth errors
if _contains "$response" "401" || _contains "$response" "Authorization"; then
_err "Authentication failed. Please check your Infoblox_UDDI_Key."
return 1
fi
# For other errors, continue to parent domain
p=$i
i=$((i + 1))
continue
fi
# Check if response contains results (even if empty)
if _contains "$response" '"results"'; then
# Extract zone ID - must match the pattern dns/auth_zone/...
zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
if [ -n "$zone_id" ]; then
# Found the zone
_domain="$h"
_domain_id="$zone_id"
# Calculate subdomain
if [ "$_domain" = "$domain" ]; then
_sub_domain=""
else
_cutlength=$((${#domain} - ${#_domain} - 1))
_sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength")
fi
return 0
fi
fi
p=$i
i=$((i + 1))
done
return 1
}
# _infoblox_rest GET "dns/record?_filter=..."
# _infoblox_rest POST "dns/record" "{json body}"
# _infoblox_rest DELETE "dns/record/uuid"
_infoblox_rest() {
method=$1
ep="$2"
data="$3"
_debug "$ep"
# Ensure credentials are available (when called from _get_root)
Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}"
Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}"
Infoblox_UDDI_Api="https://$Infoblox_Portal/api/ddi/v1"
export _H1="Authorization: Token $Infoblox_UDDI_Key"
export _H2="Content-Type: application/json"
# Debug (masked)
_tok_len=$(printf "%s" "$Infoblox_UDDI_Key" | wc -c | tr -d ' \n')
_debug2 "Auth header set" "Token len=${_tok_len} on $Infoblox_Portal"
if [ "$method" != "GET" ]; then
_debug data "$data"
response="$(_post "$data" "$Infoblox_UDDI_Api/$ep" "" "$method")"
else
response="$(_get "$Infoblox_UDDI_Api/$ep")"
fi
_ret="$?"
_debug2 response "$response"
if [ "$_ret" != "0" ]; then
_err "Error: $ep"
return 1
fi
return 0
}
Loading…
Cancel
Save