Compare commits

...

660 commits

Author SHA1 Message Date
neil
261653ebf4
Merge pull request #7194 from acmesh-official/dev
Some checks are pending
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Nginx / Nginx (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenEuler / OpenEuler (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
sync
2026-08-13 17:01:02 +08:00
Pablo
41bdd4cd0e
Add UniFi OS Server deploy hook (#7184)
Some checks are pending
DNS / CheckToken (push) Waiting to run
DNS / Fail (push) Blocked by required conditions
DNS / Docker (push) Blocked by required conditions
DNS / MacOS (push) Blocked by required conditions
DNS / Windows (push) Blocked by required conditions
DNS / FreeBSD (push) Blocked by required conditions
DNS / GhostBSD (push) Blocked by required conditions
DNS / OpenBSD (push) Blocked by required conditions
DNS / NetBSD (push) Blocked by required conditions
DNS / DragonFlyBSD (push) Blocked by required conditions
DNS / MidnightBSD (push) Blocked by required conditions
DNS / Solaris (push) Blocked by required conditions
DNS / Omnios (push) Blocked by required conditions
DNS / OpenIndiana (push) Blocked by required conditions
DNS / Tribblix (push) Blocked by required conditions
DNS / Haiku (push) Blocked by required conditions
DNS / Hurd (push) Blocked by required conditions
DNS / OpenEuler (push) Blocked by required conditions
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
* Add UniFi OS Server deploy hook

Uses UniFi OS Server's local REST API (login, list, upload, activate,
remove superseded) since it stores certificates in its own Postgres
database rather than flat config files, unlike the Cloud Key/UDM
hardware covered by the existing unifi deploy hook. Tested against
real instances on both macOS and Ubuntu 26.04 (self-hosted, remote).

* Address review: portable sed/grep, scoped HTTPS_INSECURE, fingerprint matching

- Replace GNU-only \n in sed replacement with a portable literal newline
  (matches dnsapi/dns_cpanel_uapi.sh, dnsapi/dns_glesys.sh); pipe the
  list response through _normalizeJson first for consistent formatting.
- Use grep -F for the domain-name match instead of an unescaped BRE --
  a wildcard cert name (*.example.com) broke the regex.
- Drop \W (undocumented, GNU-only) from the cookie lookup in favor of
  an anchored `^Set-Cookie: *NAME=` match.
- Scope HTTPS_INSECURE=1 inside the hook (matches deploy/proxmoxve.sh,
  deploy/fritzbox.sh) instead of requiring the caller to export it for
  the whole acme.sh run, which would also disable verification for the
  connection to the ACME CA.
- On a duplicate-certificate response, match the existing entry by
  fingerprint instead of taking the first name match -- with more than
  one stale entry for a domain, the wrong one could get activated.
- Check the list endpoint's response code before proceeding.
- Save username/password with the "base64" flag (matches
  deploy/synology_dsm.sh) since _save_conf wraps values in unescaped
  single quotes.

* Rework certificate handling: unique names per upload, drop cleanup

Testing against a real UniFi OS Server showed the server enforces name
uniqueness independently of fingerprint uniqueness, and that activation is
exclusive server-wide regardless of name/domain. A unique name per upload
avoids the name-collision path entirely (previously only handled as a
retry-of-identical-content edge case), and removes the need for the
post-hoc cleanup loop, which risked deleting the wrong entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Shorten generated certificate name to Unix epoch seconds

Real-hardware testing showed the UniFi OS Server certificate list's name
column is fixed-width and doesn't wrap, so a full human-readable timestamp
overlaps the Expires column and makes both unreadable. Epoch seconds are
still short enough to fit while remaining unique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add scoped cleanup of old certificate entries, use _time helper

Per review: dropping cleanup entirely went further than the original bug
required, and left old entries (each holding a private key) accumulating
indefinitely. Since every upload now gets a name unique to its domain and
run, cleanup can safely target only entries whose name starts with that
domain -- entries this hook itself created -- excluding the one just
activated. Also swaps date +%s for the core _time helper, and rewrote the
design comments to make them clearer and match the current behavior
instead of the pre-redesign one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 14:57:44 +08:00
XuChao
a89ba9c2e5
add deploy hook support for ikuai (#6456)
* add deploy hook support for ikuai

* fix shellcheck warn and shfmt the code

* 1.fix config load 2.use _secre_debug2 to log password 3.use fullchain to deploy 4.fix hardcode id 5.change shebang

* fix miss ; after cookie

* 1.fix shfmt ; 2.fix IKUAI_CERT_ID conf load; 3.correct IKUAI_CERT_ID description

* fix some log msg

* fix shfmt
2026-08-13 14:52:48 +08:00
Zhiwei Liang
2e2782f0d8
Remove deprecated Linode API v3 DNS plugin (#7054)
Signed-off-by: Zhiwei Liang <zhiwei.liang@zliang.me>
2026-08-12 22:47:58 +08:00
neil
4a3bc2c919 Fix dns_netcup reporting a bogus 4013 instead of the real zone error
Some checks failed
DNS / CheckToken (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
DNS / Hurd (push) Has been cancelled
DNS / OpenEuler (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
The zone lookup walked the challenge name from the right and ended up
asking netcup for the full "_acme-challenge.<domain>" as a zone name.
That can never be a zone, so netcup answered 4013 "Validation Error",
which replaced the real 5028 "The zone <domain> could not be found" as
the error shown to the user.

Stop one label short of the full name, and fail explicitly when no zone
matched, reporting the last API response plus what to check. Before, a
run where every candidate returned 5028 fell through to logout and
returned success.
2026-08-10 10:00:26 +08:00
neil
05367d3598 Listen on both IPv4 and IPv6 in standalone mode by default
Some checks failed
Linux / Linux (opensuse/leap:latest) (push) Has been cancelled
Linux / Linux (oraclelinux:8) (push) Has been cancelled
Linux / Linux (ubuntu:latest) (push) Has been cancelled
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Nginx / Nginx (push) Has been cancelled
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenEuler / OpenEuler (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
PebbleStrict / PebbleStrict (push) Has been cancelled
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
socat binds a single family unless told which one: up to 1.7.x the
default IP version for TCP-LISTEN is 4, and 1.8.0 made it "no
preference", which resolves to whatever getaddrinfo and bindv6only
happen to give. So an order carrying both an IPv4 and an IPv6
identifier could never pass both http-01 challenges.

Bind one socket per family instead, with ipv6only on the IPv6 one so
the two do not collide. IPv4-mapped IPv6 addresses are not a portable
alternative, OpenBSD does not support them at all. The IPv6 listener
is best effort, a host without IPv6 still gets the IPv4 one. The
python fallback does the same. --listen-v4 and --listen-v6 keep
forcing a single family, and passing both now means both.

Le_Listen_V4 and Le_Listen_V6 were mutually exclusive in the domain
conf, which silently dropped one of them on renewal, and
_starttlsserver let -4 win when both were set.

Fixes #7185
2026-08-08 13:09:30 +08:00
Alexey Morozov
f1cbba05f6
feat: added nexdns dnsapi (#7170)
Some checks failed
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
DNS / CheckToken (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
DNS / Hurd (push) Has been cancelled
DNS / OpenEuler (push) Has been cancelled
* feat: added nexdns dnsapi

Adds a DNS-01 hook for NexDNS, an authoritative DNS service with a REST API.

dns_nexdns_add walks the label list to find the zone that owns the challenge
name and creates the TXT record in it. dns_nexdns_rm lists the TXT records at
that name, picks the one carrying exactly this challenge value and deletes it
by id, so a wildcard and its base domain do not remove each other's record.

A 429 is waited out and the request retried, in the shape dns_hetznercloud.sh
and dns_bunny.sh already use.

* dns_nexdns: cap the rate-limit wait, judge success by status, add the tracking issue
2026-08-07 09:22:28 +08:00
neil
f67be78ff4 Fix dns_namecheap ignoring IsOurDNS when matching the root zone
_get_root_by_getList() matched the candidate suffix as an unanchored
substring of the whole domains.getList response and never looked at the
IsOurDNS attribute. A domain parked on Namecheap's webhosting DNS is
listed with IsOurDNS="false", yet it was still accepted as the root zone,
so _get_root() returned success and the domains.dns.getHosts probe that
would have found the real zone never ran. Every following getHosts call
was then refused with error 2030288 "not using proper DNS servers" and
the challenge failed with "invalid tld".

Match the exact <Domain Name="..."> entry instead and require
IsOurDNS="true", so a subdomain delegated to Namecheap BasicDNS/FreeDNS
under a parent that is not on Namecheap DNS now resolves to its own zone.
Matching the entry exactly also drops the old substring/regex match, in
which the dots of a domain matched any character.

Fixes #7178
2026-08-06 19:36:26 +08:00
neil
9aad4dcbd5 Fix multideploy MULTIDEPLOY_FILENAME conf read and allow an absolute path
Some checks failed
Linux / Linux (debian:latest) (push) Has been cancelled
Linux / Linux (fedora:latest) (push) Has been cancelled
Linux / Linux (gentoo/stage3) (push) Has been cancelled
Linux / Linux (kalilinux/kali) (push) Has been cancelled
Linux / Linux (opensuse/leap:latest) (push) Has been cancelled
Linux / Linux (oraclelinux:8) (push) Has been cancelled
Linux / Linux (ubuntu:latest) (push) Has been cancelled
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Nginx / Nginx (push) Has been cancelled
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenEuler / OpenEuler (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
PebbleStrict / PebbleStrict (push) Has been cancelled
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
_getdeployconf assigns and exports the variable, it does not print the
value, so wrapping it in a command substitution ran it in a subshell and
always yielded an empty string. A MULTIDEPLOY_FILENAME saved by an
earlier run was therefore never restored on renewal and the hook
silently fell back to multideploy.yml. Call it the same way every other
deploy hook does.

Also treat a MULTIDEPLOY_FILENAME starting with '/' as an absolute path
instead of always resolving it under DOMAIN_PATH, so one deploy file can
live outside the certificate directory and be shared by all domains.
Names without a leading '/' keep resolving under DOMAIN_PATH as before.
2026-08-05 23:01:54 +08:00
neil
603a126a7c Fix synology_dsm logging out after the temp admin is already deleted
_temp_admin_cleanup ran before _logout, so the logout request carried
the session id of an account synouser had already removed and DSM kept
the orphaned entry in Connected Users. Swap the order in both terminal
branches, and add the missing _logout to the two post-login error paths
(CRT list failure, certificate not found without SYNO_CREATE).

_logout overwrites the global $response, so the upload-failure branch
prints its error message before calling it.

Reported by @Bertl75 in #7174
2026-08-05 19:56:13 +08:00
neil
5e6c263211 Fix empty finalize URL when resuming a saved DNS-manual order
The decision to resume a pending order is keyed on Le_Vlist, but the
decision to keep Le_OrderFinalize/Le_LinkOrder was keyed on the webroot
being exactly "dns". Any other webroot with a saved Le_Vlist skipped
newOrder and then finalized against an empty URL.

Key both on Le_Vlist, and always clear Le_LinkCert, which is per-run
state that is never read back from the saved domain conf.

Fixes #7177
2026-08-05 19:53:52 +08:00
neil
b4925052dd Fix dns_cyon cleanup failing on FreeBSD
Some checks failed
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
DNS / Hurd (push) Has been cancelled
DNS / OpenEuler (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
_cyon_delete_txt relied on `printf "%b"` to convert a sed-injected literal
`\n` into a real newline, but `%b` also processes the `\"` escapes that the
JSON response is full of. glibc/bash/dash keep the backslash of such an
undefined escape, FreeBSD's printf (sh builtin and /usr/bin/printf alike)
drops it -- so `data-hash=\"..\"` became `data-hash=".."`, the extraction
regex matched nothing, _dns_entries stayed empty and no TXT record was ever
deleted.

Drop the newline injection and use _egrep_o, which already yields one match
per line, then parse each line with sed.

Also feed the read loop a newline-terminated list: `printf "%s"` left the
last line unterminated, so `read` returned non-zero at EOF and the loop
skipped the final entry on every platform.

Verified identical output on FreeBSD 14.3, Linux/bash and Linux/dash.

Fixes #7169
2026-08-02 21:55:52 +08:00
neil
ea5e70564d Fix --make-dns-persist-value printing a wildcard TXT record name
For -d '*.example.com' the printed record name kept the literal '*' label
(_validation-persist.*.example.com). The CA never queries that name, so
issuance fails with "No TXT record found for DNS-PERSIST-01 challenge".

Per draft-ietf-acme-dns-persist-01 sec 4 and 10.2 the record is published at
the base domain's Validation Domain Name; the wildcard scope comes from
'policy=wildcard' in the record value (sec 5.1), not from a '*' label in the
record name. Strip the leading "*." in a new _dns_persist_txt_name helper,
and imply --dns-persist-wildcard for a wildcard -d, since without
policy=wildcard the printed record can never authorize the wildcard.

Fixes #7168
2026-08-02 21:47:51 +08:00
Joel Samson
bf90b845b2
Refactor dns_freemyip.sh for enhanced compatibility (#7166)
* Refactor dns_freemyip.sh for clarity and compatibility

Updated dns_freemyip.sh for better readability and compatibility with ASUSWRT-Merlin. Improved error handling and response logging.

* Update author information in dns_freemyip.sh

* replace both loops with POSIX shell counters

replace both loops with POSIX shell counters

* Typo

Typo

* Fix error message for freemyip API request failure

Remove existing token leak. Not my regression.

* Refactor retry logic and improve error handling

* Remove unnecessary blank lines in dns_freemyip.sh

* Clean up dns_freemyip.sh by removing blank lines

Removed unnecessary blank lines in the script to improve readability.
2026-08-02 21:34:41 +08:00
Goncharenko Alexander
0565443622
dns_yc: fix TXT record removal failing with "Unknown key file format" (#7150)
Some checks failed
DNS / CheckToken (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
DNS / Hurd (push) Has been cancelled
DNS / OpenEuler (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
* dns_yc: restore YC_SA_Key_File in dns_yc_rm before signing the JWT

dns_yc_rm() never rebuilt YC_SA_Key_File from YC_SA_Key_File_PEM_b64 /
YC_SA_Key_File_Path like dns_yc_add() does. Per the DNS API dev guide,
add()/rm() run in separate subshells, so rm() must repeat add()'s setup
steps rather than rely on variables set during add().

Without it, when _yc_login() needs a fresh JWT during removal (the IAM
token from the add phase isn't available), it signs with an empty/unset
key path, and openssl fails with "Unknown key file format". The
resulting auth failure then surfaces misleadingly as "invalid domain" in
_get_root, and the TXT record is never deleted.

Verified against a real Yandex Cloud account/zone with --staging: before
the fix, removal failed with the same errors reported in the issue;
after adding the missing key-restoration block, add + remove both
succeed and the TXT record is actually deleted.

* dns_yc: preserve other TXT values when removing one at the same name

dns_yc_rm previously sent the full current data array (all existing
TXT values at the name) to the deletions API, wiping out the whole
rrset instead of only the value being removed. This breaks wildcard +
base domain issuance, where both share the same _acme-challenge name
with two different values: removing the first one deleted both,
leaving nothing for the second removal to find.

* dns_yc: read persisted config from domain conf before account conf

YC_Zone_ID, YC_Folder_ID, YC_SA_ID, YC_SA_Key_ID (zone-ID mode) and
YC_SA_Key_File_PEM_b64/Path were always saved via _savedomainconf
(domain.conf), but only ever read back via _readaccountconf_mutable
(account.conf). Once the env vars were unset, none of these could be
recovered from the saved config, so dns_yc_add/dns_yc_rm failed with
"You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File."
even though the values had been persisted correctly on the prior run.

* dns_yc: replace grep -Fxv/sed with a portable loop in dns_yc_rm

Solaris's /usr/bin/grep supports neither -F nor -x, so
_remaining_txtvalue was always empty there and the preserve-other-
values logic silently fell back to deleting the whole rrset (with a
grep usage error on stderr on every rm). The sed trailing-comma strip
had a matching issue on Solaris, whose sed drops an unterminated last
line. CI didn't catch this because the fallback path also returns
"done: true". Use a plain for-loop with word splitting instead.

* dns_yc: use upsertRecordSets.deletions to remove a single TXT value

updateRecordSets has no "merges" field (only deletions/additions), so
the previous preserve-other-values logic silently did nothing -- the
TXT record was never actually removed, a regression from before that
change (which at least deleted the whole rrset). CI didn't catch it
because _clearupdns runs dns_yc_rm in a subshell and ignores its exit
code.

upsertRecordSets.deletions removes only the specified value from the
rrset directly, so the getRecordSet read and the remaining-value
recomputation are no longer needed at all.

Verified against a real zone (base + wildcard domain sharing one
_acme-challenge name): adding both values then removing one leaves
the other in place, and removing the second cleans up fully.

* dns_yc: don't delete the user's own key file in YC_SA_Key_File_Path mode

_yc_login unconditionally rm'd $YC_SA_Key_File after signing. That's
fine for the PEM_b64 path, where it's a decoded temp file, but in
YC_SA_Key_File_Path mode it's the user's own persistent key file --
the first successful login permanently deleted it, so every
subsequent dns_yc_rm/renewal hit "Unknown key file format" (the exact
symptom this PR is about, just from a different cause). Track whether
the key file is our own temp copy and only delete it in that case.

Verified with a stubbed _yc_login: a temp-mode key gets removed after
login, a path-mode key survives.

* dns_yc: clear both domain and account conf on invalid config

The failure branch in dns_yc_add only ever called _clearaccountconf,
but YC_Zone_ID/YC_Folder_ID/YC_SA_Key_File_PEM_b64/Path are persisted
via _savedomainconf, and YC_SA_ID/YC_SA_Key_ID may have been saved via
_saveaccountconf_mutable (Folder_ID mode, which stores under a
SAVED_ prefix read back by _readaccountconf_mutable). Clearing only
one store left stale values behind in whichever one wasn't touched.

Verified by seeding both domain.conf and account.conf with leftover
values, then triggering this branch and confirming both config files
end up empty.
2026-07-28 10:04:20 +08:00
neil
057c940895 add OpenEuler
Some checks failed
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
DNS / Hurd (push) Has been cancelled
DNS / OpenEuler (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
2026-07-26 15:01:41 +08:00
neil
7c12deb7ef fix: grep -A is not portable, breaks ARI on Solaris
Solaris /usr/bin/grep has no -A ("illegal option -- A"), so _getAKI
printed an error to stderr on every cron renewal and returned empty.
The empty AKI silently corrupts the RFC 9773 ARI certID, so ARI is
never available and renewal falls back to the fixed schedule.

Split the pipeline into a testable stdin filter _extractAKI and select
the value line with a portable sed range instead.

Same fix for the two hooks that still used grep -A: dns_world4you.sh
(also replaces the GNU-only "\s" in the same expression) and
deploy/keyhelp.sh (the -A 2 window could truncate the div range that
follows it, so it is just dropped).

https://github.com/acmesh-official/acme.sh/issues/7159
2026-07-25 16:00:03 +08:00
neil
830782fd1d fix dns_yc: avoid empty-matchable _egrep_o pattern that hangs OmniOS
Some checks are pending
DNS / CheckToken (push) Waiting to run
DNS / Fail (push) Blocked by required conditions
DNS / Docker (push) Blocked by required conditions
DNS / MacOS (push) Blocked by required conditions
DNS / Windows (push) Blocked by required conditions
DNS / FreeBSD (push) Blocked by required conditions
DNS / GhostBSD (push) Blocked by required conditions
DNS / OpenBSD (push) Blocked by required conditions
DNS / NetBSD (push) Blocked by required conditions
DNS / DragonFlyBSD (push) Blocked by required conditions
DNS / MidnightBSD (push) Blocked by required conditions
DNS / Solaris (push) Blocked by required conditions
DNS / Omnios (push) Blocked by required conditions
DNS / OpenIndiana (push) Blocked by required conditions
DNS / Tribblix (push) Blocked by required conditions
DNS / Haiku (push) Blocked by required conditions
DNS / Hurd (push) Blocked by required conditions
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
OmniOS native egrep -o infinite-loops emitting empty lines when the
pattern can match the empty string, so `_egrep_o "[^:]*$"` never lets
the pipeline finish and dns_yc hangs until the CI timeout. Require at
least one character instead. `+` is not usable because the sed fallback
in _egrep_o parses BRE.
2026-07-25 13:34:06 +08:00
Qhilm
698f6c7329
Feat: Shelly deploy hook for firmware 2.0.0+ (#7145)
* feat: add Shelly Gen3+ deploy hook with RFC 7616 HTTP Digest auth

Adds deploy/shelly.sh for deploying Let's Encrypt HTTPS server certificates
to Shelly Gen3+ devices (Gen4 tested) via JSON-RPC over HTTP.

- RFC 7616 SHA-256 HTTP Digest authentication (Authorization header)
- Uploads fullchain.pem and private key via Shelly.PutHTTPServerCert / PutHTTPServerKey
- Auto-reboot support (SHELLY_REBOOT to disable)
- Auth auto-detection: no password = no auth, password = Digest
- Nonce counter (nc) increments per request per RFC 7616
- Tested against Shelly 2PM Gen4 (firmware 2.0.0)

Also adds deploy/test_shelly.sh for self-testing the hook logic without
a real device (mocked _post).

* fix: address review feedback on shelly deploy hook

- Fix _secure_debug calls to use two arguments (label + value)
- Remove bash-only $RANDOM cnonce fallback; openssl always available
- Parse $HTTP_HEADER directly instead of raw curl re-request
- Detect auth via HTTP 401 status line, not empty response body
- Route reboot through _shelly_rpc to rebuild auth header with correct nc
- Remove export HTTPS_INSECURE=1 (no-op for http://, leaks to other hooks)
- Clear _H1 before returning from shelly_deploy
- Prefix all helper variables with _shelly_ to avoid namespace collisions
- Delete deploy/test_shelly.sh (deploy/ files become hook names)
- Fix missing trailing newline

* fix: validate shelly JSON-RPC responses are valid JSON

Non-JSON responses like HTTP 429 'Too Many Requests' would pass
the empty-response and '"error"' checks and be reported as success.
Now reject any response that doesn't start with '{' and contain '"id"'.

* fix: add 1s delay between shelly cert/key clear and upload calls

The Shelly device has a race condition where uploading data immediately
after clearing the existing cert/key returns -103 'Missing required
argument data!'. A 1-second delay fixes this.

* fix: remove clear-before-upload in shelly deploy hook

Shelly auto-removes all three TLS files (cert, key, CA bundle) when any
single one is cleared. The old sequence clear-cert → upload-cert →
clear-key → upload-key resulted in the key clear wiping the newly
uploaded cert, leaving only the key at boot time. The mbedtls
pk_check_pair then silently skipped the HTTPS listener.

Fix: just upload directly (overwrite in place). No clearing needed.

* Fix ShellCheck SC2090 and shfmt in shelly deploy hook

SC2090: false positive on export _H1 (used quoted in _post)
shfmt: no space after "<" in _json_encode redirects

* moved  two lines to cover the whole if block

---------

Co-authored-by: neil <github@neilpang.com>
Co-authored-by: cysimons <cysimons@cisco.com>
2026-07-25 13:03:56 +08:00
Alec Höfler
749fa2487e
feat: added realtoxmedia dnsapi (#7156)
Some checks are pending
DNS / CheckToken (push) Waiting to run
DNS / Fail (push) Blocked by required conditions
DNS / Docker (push) Blocked by required conditions
DNS / MacOS (push) Blocked by required conditions
DNS / Windows (push) Blocked by required conditions
DNS / FreeBSD (push) Blocked by required conditions
DNS / GhostBSD (push) Blocked by required conditions
DNS / OpenBSD (push) Blocked by required conditions
DNS / NetBSD (push) Blocked by required conditions
DNS / DragonFlyBSD (push) Blocked by required conditions
DNS / MidnightBSD (push) Blocked by required conditions
DNS / Solaris (push) Blocked by required conditions
DNS / Omnios (push) Blocked by required conditions
DNS / OpenIndiana (push) Blocked by required conditions
DNS / Tribblix (push) Blocked by required conditions
DNS / Haiku (push) Blocked by required conditions
DNS / Hurd (push) Blocked by required conditions
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
2026-07-24 13:55:17 +08:00
Radu
24d573b6d3
Merge pull request #7140 from radumalica/dns_hestiacp
Some checks failed
DNS / CheckToken (push) Waiting to run
DNS / Fail (push) Blocked by required conditions
DNS / Docker (push) Blocked by required conditions
DNS / MacOS (push) Blocked by required conditions
DNS / Windows (push) Blocked by required conditions
DNS / FreeBSD (push) Blocked by required conditions
DNS / GhostBSD (push) Blocked by required conditions
DNS / OpenBSD (push) Blocked by required conditions
DNS / NetBSD (push) Blocked by required conditions
DNS / DragonFlyBSD (push) Blocked by required conditions
DNS / MidnightBSD (push) Blocked by required conditions
DNS / Solaris (push) Blocked by required conditions
DNS / Omnios (push) Blocked by required conditions
DNS / OpenIndiana (push) Blocked by required conditions
DNS / Tribblix (push) Blocked by required conditions
DNS / Haiku (push) Blocked by required conditions
DNS / Hurd (push) Blocked by required conditions
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
Hurd / Hurd (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
feat: add dnsapi for HestiaCP
2026-07-23 11:50:20 +08:00
neil
1774d838ca add GNU hurd 2026-07-22 20:42:14 +08:00
neil
4c8a143086 fix proxmoxve/proxmoxbs deploy: fail on non-2xx API response
Some checks failed
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
The success check only grepped "message" from the response body, but
PVE/PBS auth failures return HTTP 401 with an empty body, so wrong or
unauthorized API tokens were reported as "Certificate successfully
deployed". Also _retval captured the exit code of the message pipeline
instead of _post. Check the HTTP status line from $HTTP_HEADER and
capture _post's exit code directly.

fix https://github.com/acmesh-official/acme.sh/issues/7141
2026-07-20 10:02:54 +08:00
neil
6feb1df83c fix cpanel_uapi: pass --user to DomainInfo list_domains when run as root
Some checks are pending
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
The auto mode sitelist query was missing the --user branch that the
install_ssl calls already have, so deploy always failed under root.
fix https://github.com/acmesh-official/acme.sh/issues/7139
2026-07-19 10:20:58 +08:00
neil
97c5aca136 add cache-after-prepare: true
Some checks failed
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
PebbleStrict / PebbleStrict (push) Has been cancelled
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
2026-07-18 09:32:26 +08:00
neil
7fa3018219 feat: add ACME_PACKAGED for distro-packaged installs
When ACME_PACKAGED is set (e.g. exported by a distro package wrapper):
- --install does not copy the script or the hooks into LE_WORKING_DIR;
  the cron job and the shell alias point to the packaged script instead
- --upgrade, --install-online and the cron AUTO_UPGRADE path refuse and
  point to the system package manager
- --uninstall does not remove the packaged files

https://github.com/acmesh-official/acme.sh/issues/7135
2026-07-17 22:03:14 +08:00
neil
9bfcf2cf1a
Merge pull request #7128 from achmadalifn4/master
Fix Notify Waha
2026-07-17 21:58:41 +08:00
neil
2feb392bd0
Merge pull request #7133 from acmesh-official/dev
Some checks failed
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
sync
2026-07-17 15:05:00 +08:00
neil
59a97d7f8b fix bug for solaris.
Some checks are pending
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Nginx / Nginx (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
dnsapi/deploy: remove POSIX character classes from sed/grep patterns

Solaris /usr/bin/sed and /usr/bin/grep parse [[:space:]] etc. as a
literal bracket set and silently mis-match. Replace with [ ]* for
JSON matching, a printf-tab bracket for user-input trimming, and
[0-9] for digits; also drop GNU-only sed -r/-E in rage4, selfhost
and selectel, and reuse _strip_blank_lines in byteplus_alb.
2026-07-17 12:38:14 +08:00
neil
a836e747d1 start 3.1.5 2026-07-17 10:10:07 +08:00
neil
3661fd86b6
Merge pull request #7132 from acmesh-official/dev
Some checks are pending
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Nginx / Nginx (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
sync
2026-07-17 10:01:37 +08:00
neil
745a42f193 minor 2026-07-16 20:09:10 +08:00
ACHMAD ALIF NASRULLOH
8f98786d93
Merge branch 'acmesh-official:master' into master 2026-07-16 11:29:49 +07:00
ACHMAD ALIF NASRULLOH
6d559ae69f
Add newline at end of waha.sh
Fix missing newline at end of file.
2026-07-16 11:29:39 +07:00
ACHMAD ALIF NASRULLOH
24501ac0ca
Merge branch 'dev' into master 2026-07-16 11:27:57 +07:00
Achmad Alif Nasrulloh
a82cf763cf fix(notify): remove duplicate Content-Type header in waha hook 2026-07-16 11:23:18 +07:00
neil
3faf65c46d fix https://github.com/acmesh-official/acme.sh/issues/1940#issuecomment-4971257867
Some checks are pending
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Nginx / Nginx (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
2026-07-15 20:41:35 +08:00
neil
7cc16cd09a
Merge pull request #7126 from acmesh-official/dev
Some checks failed
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
sync
2026-07-14 22:35:07 +08:00
neil
1dbabf0da9 valid-to: allow a negative --days to set the renewal margin
Some checks failed
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
DNS / CheckToken (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
A negative --days is anchored to the expiry, so it composes with a
relative --valid-to: "--valid-to +30d --days -7" renews 7 days before
the expiry instead of the hardcoded 1 day. A positive --days and any
--days with a fixed-date --valid-to are still rejected.

https://github.com/acmesh-official/acme.sh/issues/6570
2026-07-14 21:12:55 +08:00
Avi Miller
2499ee1ab9
fix(dns_oci): read ~/.oci/config before cached account.conf values (#7124)
The OCI DNS plugin cached the tenancy, user, region and signing key into
acme.sh's account.conf at issuance and then, on subsequent runs, read those
cached values before consulting ~/.oci/config. A value cached at issuance
therefore permanently shadowed the config file, so editing ~/.oci/config
afterwards (most visibly rotating the API signing key) had no effect and
renewals kept using stale credentials, failing authentication.

Resolve each field in the order: explicit environment variable, then
~/.oci/config when it exists, then the cached account.conf value as a
fallback for env-only installs that have no config file. The signing key
likewise prefers the key_file resolved from the environment or ~/.oci/config
over any cached key.

Signed-off-by: Avi Miller <me@dje.li>
2026-07-14 20:58:47 +08:00
NotAnotherHelloWorld
1a746d98b8
Deploy certificate to FortiGate firewall using API (#6236)
Some checks are pending
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
* Deploy certificate to FortiGate firewall using API

* Refactor FortiGate deployment functions

Prefix private functions and working variables and use a timestamped certificate name.

* Replace grep -o for POSIX compatibility
2026-07-14 09:02:34 +08:00
neil
4558a8aa98 challenge: use portable [{] literal-brace in _egrep_o patterns (#968)
Some checks failed
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
PebbleStrict / PebbleStrict (push) Has been cancelled
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
_egrep_o falls back to a BRE sed expression on shells without egrep -o
(Solaris, DD-WRT busybox). A bare "\{" there is a BRE interval operator
and aborts ("sed: command garbled" / "Invalid content of \{\}"), so the
challenge-status-invalid path extracted an empty error object and the
CA's failure reason was lost. Replace the escaped braces with "[{]"/"[}]"
bracket expressions, unambiguous literal braces in both BRE and ERE, at
all four call sites (challenge type/error extraction and profiles).
2026-07-13 11:59:35 +08:00
neil
1a54307dbf account: keep restored ACCOUNT_URL in the EAB-already-bound path
When re-registering an already-bound EAB account (HTTP 400 "not
awaiting external account binding"), the else branch restored
ACCOUNT_URL from ca.conf but the following unconditional
`export ACCOUNT_URL="$_accUri"` clobbered it with an empty _accUri
(never set on that path), so later signed requests failed with
"A Key ID MUST be specified" / "account URL is empty". Assign the
restored value to _accUri so the shared export uses it.
https://github.com/acmesh-official/acme.sh/issues/3382
2026-07-13 10:43:08 +08:00
neil
020123d812 dns_infomaniak: log zones response and fail early in _get_zone
The sed in _get_zone passed the raw JSON through when the response
contained no [{"fqdn":", so an API error turned the zone into "{" and
the failure only surfaced later as POST /2/zones/{/records
"method_not_found". Log the response at debug2, error out on
non-success results, and parse fqdn position-independently.

https://github.com/acmesh-official/acme.sh/issues/6851
2026-07-13 08:30:43 +08:00
neil
17964cfd6e installcronjob: Solaris grep takes only one -e, use separate greps
The "no crontab" whitelist used multiple -e patterns, but Solaris
/usr/bin/grep honors only a single -e, so a fresh install was
refused there. Use one plain grep per message pattern, which every
grep implementation supports (caught by le_test_installcronjob_no_wipe
on the Solaris CI).
2026-07-12 22:28:56 +08:00
neil
d621d6952a fix 2026-07-12 20:18:36 +08:00
neil
5e33e9f5f1 dns_knot: add KNOT_ZONE for delegated subdomain zones
The zone cannot be derived from the record name when the Knot server
is only authoritative for a delegated subdomain; let the user name it
explicitly, like NSUPDATE_ZONE.

fixes https://github.com/acmesh-official/acme.sh/issues/2881
2026-07-12 19:22:03 +08:00
neil
3fddea2962 dns_dynu: use mutable conf storage so newly exported credentials win
Same stale-account.conf override as dns_dnsimple: the sourced plain
copy shadowed newly exported Dynu_ClientId/Dynu_Secret.

https://github.com/acmesh-official/acme.sh/issues/3216
2026-07-12 19:21:56 +08:00
neil
a6766d4186 dns_dnsimple: use mutable conf storage so a newly exported token wins
The legacy plain _saveaccountconf copy in account.conf is sourced at
startup and silently overrides a newly exported DNSimple_OAUTH_TOKEN,
so rotated tokens never took effect.

fixes https://github.com/acmesh-official/acme.sh/issues/3422
2026-07-12 19:21:48 +08:00
neil
b9ce911eb1 installcronjob: never wipe existing cron jobs when crontab -l fails
Piping a failed 'crontab -l' straight back into 'crontab -' replaced
the whole crontab with just the acme.sh entry when the listing failed
while jobs existed (seen on cPanel/CloudLinux jailshell). Capture the
listing first and refuse to write unless the failure is the normal
"no crontab for user" case.

https://github.com/acmesh-official/acme.sh/issues/3079
2026-07-12 19:21:40 +08:00
neil
15a1067f1b update-account: persist the new email into the CA conf
"--update-account -m new@example.com" updated the contact on the CA but
never saved it locally, so CA_EMAIL kept showing the old address on
every later run. Save it in the success path like _regAccount does.

https://github.com/acmesh-official/acme.sh/issues/4673
2026-07-12 17:34:37 +08:00
neil
447dc3c7e7 dns_ali: convert IDN domains to punycode
The Aliyun API only accepts punycode domain names, and a raw UTF-8
domain also breaks the request signature. Same _idn pattern as
dns_namecom.

https://github.com/acmesh-official/acme.sh/issues/4733
2026-07-12 17:21:19 +08:00
Kat Crichton-Seager
5a8c685fd3
fix: dnsexit api rejects a TTL of zero, changed to 1 (minute) (#7107) 2026-07-12 16:37:21 +08:00
amk1969
dd6540ce46
dns_ovh specific permission for record removal (#6386)
Co-authored-by: amk <amk@medlin.mikus.sk>
2026-07-12 16:21:00 +08:00
Steven Qiu
cd486cfbb9
Add Baidu Cloud CDN deploy hook (#6951)
* add Baidu Cloud CDN deploy hook

Code generated by GitHub Copilot with Claude Sonnet 4.6. Tested with local environment by human.

* inline functions

Code generated by OpenAI Codex with GPT-5.5 Sol. Tested with local environment by human.
2026-07-12 16:15:59 +08:00
matthias-matze
e828b285ad
Matthiasvpfr patch 1 (#7108)
* Add files via upload

* New Banner

Updated README to include responsive images for dark and light modes.

* Add files via upload

Remove usage of jq and curl
should be compliant with acme.sh api dev guide

* Add files via upload

* Add files via upload

change CR LF to LF

* Add files via upload

missing CR

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

correct auth

* Add files via upload

better manage group_id

* Add files via upload

manage wrong / missing domain

* Add files via upload

strip domain to manage subdomain requests

* Add files via upload

fix wrong id selection in get root

* Add files via upload

fix parsing of IDs

* Add files via upload

correct get_root to better handle unexisting domaines (acmetest)

* Add files via upload

correct token incorrect in auth

* Add files via upload

manage case web api reply is empty

* Add files via upload

try to resolve error when adding unexisting subdmain txt entry

* Add files via upload

correct domain parsing

* Add files via upload

revert changes when adding record (name)
change rm to better handle complex urls

* Add files via upload

correct rm function to better manage records deletion

* Add files via upload

ensure auth variable arent lost during calls

* Add files via upload

try to keep autj variables accross executions

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

fix stripping subdomains

* Add files via upload

* Add files via upload

keep auth token instead of requesting it each time

* Add files via upload

debug

* Add files via upload

* Add files via upload

* Add files via upload

better manage record deletion to avoid orphans
added some debug and checks

* Add files via upload

paginated api support for rm function

* Add files via upload

* Add files via upload

delay to manage api 404

* Add files via upload

enhance parsing of records in rm

* Add files via upload

fix incorrect record detection in rm

* Add files via upload

better manage filters on api to limit errors

* Add files via upload

try to handle 404 when requesting API too frequently

* Add files via upload

* Add files via upload

sleep during auth

* Add files via upload

manage 404 errors in get_root

* Add files via upload

correct variable overide
translate comments
prefix all variables

* Add files via upload

correct variable

* Add files via upload

typo

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

correct variable naming (_comlaude prefix missing)
correct rm return code when non existing record
typo

* Add files via upload

log an info instead of an error if no dns record found in RM function.

---------

Co-authored-by: neil <github@neilpang.com>
Co-authored-by: Matthiasvpfr <matthias.dupont@vetpartners.fr>
Co-authored-by: ZeroSSL-Andreas <andreas.schuster@hidglobal.com>
2026-07-12 16:13:42 +08:00
ACHMAD ALIF NASRULLOH
f5e7e6b225
Merge pull request #7114 from achmadalifn4/master
Add notify waha support
2026-07-12 16:10:21 +08:00
Achmad Alif Nasrulloh
d45b6fe8e6 fix(notify): waha: tighten response check 2026-07-12 16:07:33 +08:00
Achmad Alif Nasrulloh
237f2d9c3b Add notify waha support 2026-07-12 16:07:33 +08:00
neil
ebb5cc4981 deploy/synology_dsm.sh: remove bashisms, keep the hook POSIX sh
The hook is sourced by acme.sh, so the bash shebang never takes
effect: under dash, `[ x == y ]` fails with "unexpected operator",
the 403 branch never triggers and 2FA-OTP login is skipped.
Replace `==` with `=` and use the standard sh shebang.
2026-07-12 16:04:44 +08:00
neil
3989eef5e2 cpanel_uapi: don't spill a redirection error when the key file is absent
Some checks are pending
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Nginx / Nginx (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
With --signcsr the private key never exists in the cert home, so every
renewal printed ".../domain.key: No such file or directory" from the
shell redirection. Skip the key read in that case; the install_ssl call
already ran with an empty key there and cPanel keeps the installed one.

https://github.com/acmesh-official/acme.sh/issues/6228
2026-07-12 13:00:12 +08:00
neil
2c51ac1c27 dns_dynu: include the server response in the authentication error
Same as the dns_cloudns change: a bare "Authentication failed" hides
the actual API error and makes reports undiagnosable.
2026-07-12 12:45:10 +08:00
neil
eacf0d6a87 issue bot: tell reporters to redact secrets before posting logs
Debug logs occasionally contain private keys or tokens (issue 6267);
the code-side leak in the haproxy hook was fixed by #6268, this adds
the missing warning to the auto-comment that asks for logs.
2026-07-12 12:32:54 +08:00
neil
099d88e6a9 dns_knot: KNOT_KEY takes the TSIG key data, not a file path
The knsupdate "key" statement wants "[alg:]name secret"; the info block
wrongly described the option as a file path.

https://github.com/acmesh-official/acme.sh/issues/6293
2026-07-12 12:22:28 +08:00
neil
897e219743 issue: never schedule the default renewal past the cert expiry
The default schedule was a fixed CertCreateTime + RenewalDays - 1 day,
which passes notAfter entirely for short-lived certs (internal CAs
today, the CA/B SC-081 47-day maximum later) and leaves an expired cert
in place for weeks. Extract the arithmetic into _calc_next_renew_time
and cap it at one day before expiry (one hour for lifetimes of 24h or
less, mirroring --valid-to scheduling). CAs with ARI are unaffected --
the ARI window still overrides afterwards.

https://github.com/acmesh-official/acme.sh/issues/6305
2026-07-12 12:12:18 +08:00
neil
a9590c5bd7 workflows: run issue/wiki automation only in the upstream repo 2026-07-12 11:49:53 +08:00
neil
c0d62eb934 wiki-guard: skip on forks (no <fork>.wiki repo to check out) 2026-07-12 11:43:07 +08:00
neil
44c045b056 notify: add the customscript hook
The wiki has documented "21. Set notification for customscript" since
2022 but the implementation (#4193) was never merged, so following the
wiki failed with "Cannot find the hook file". Same interface as
documented: the script gets subject, content and status code as three
arguments. Unlike #4193, the target script is invoked directly instead
of through eval -- the subject/content contain domain names and CA
messages, and eval would allow command injection through them.

https://github.com/acmesh-official/acme.sh/issues/6377
2026-07-12 11:36:45 +08:00
neil
e94631de44 dns_pdns: probe zones with the server-side name filter in _get_root
The unfiltered GET /zones lists every zone on the server; with large
installations (100k zones) root-zone detection took minutes per domain.
Probe each walk-up candidate with ?zone=<name> instead (exact match per
the PowerDNS API docs); servers that ignore the parameter return the
full list, which the existing check still handles.

https://github.com/acmesh-official/acme.sh/issues/6382
2026-07-12 11:20:53 +08:00
neil
a49f8c1992 mydevil: replace BSD-only cut -w with tr + plain cut
cut -w (split on whitespace) is a FreeBSD extension unknown to GNU
coreutils; squeeze blanks into tabs with tr first so the field
extraction is POSIX.

https://github.com/acmesh-official/acme.sh/issues/6452
2026-07-12 10:49:49 +08:00
neil
7d0283ca2c dns_zonomi: make the API endpoint configurable via ZM_Api
RimuHosting (which owns zonomi) exposes the identical API at
https://rimuhosting.com/dns/dyndns.jsp; an overridable endpoint serves
both providers with one hook. The default stays zonomi.com and is not
written to the account conf.

https://github.com/acmesh-official/acme.sh/issues/6475
2026-07-12 10:26:40 +08:00
neil
3e64b5e6f5
Merge pull request #7119 from acmesh-official/dev
Some checks failed
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
sync
2026-07-12 10:11:32 +08:00
neil
76811857a0 dns_cpanel: quote inner expansion in suffix strip (SC2295) 2026-07-12 09:35:22 +08:00
neil
22a5ae3ceb dns_cloudns: include the server response in the login error
https://github.com/acmesh-official/acme.sh/issues/6520
2026-07-12 09:34:59 +08:00
neil
04e0422526 precheck: log the socat recommendation with _info instead of _err
Missing socat only matters for standalone mode; the text even says the
warning can be ignored. Printing it to stderr made every --upgrade in a
cron noisy for DNS-only users who redirect stdout.

https://github.com/acmesh-official/acme.sh/issues/6525
2026-07-12 09:30:46 +08:00
neil
2215f1b988 notify: clear inherited _H1.._H5 before running each notify hook
Some checks are pending
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Nginx / Nginx (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
The dns/deploy hooks export _H1.._H5 in the main process, and the
notify hooks run in a subshell that inherits them. A hook that does
not overwrite every slot (ntfy without NTFY_TOKEN, slack, telegram,
etc.) sent the stale headers with its request, leaking another
service's Authorization credentials to the notify endpoint.

https://github.com/acmesh-official/acme.sh/issues/6801
2026-07-11 11:56:58 +08:00
neil
9366c2e065 dns_cpanel: resolve the most specific zone in _get_root
With both domain.tld and sub.domain.tld zones on the account, the first
endswith hit could pick the parent zone while cPanel stores the record
in the most specific one, so the cleanup never found the record and
left an orphaned _acme-challenge TXT entry. Pick the longest matching
zone with an exact literal suffix match (_endswith treats the needle as
a regex, letting xdomain.tld wrongly match zone domain.tld).

https://github.com/acmesh-official/acme.sh/issues/6807
2026-07-11 11:42:32 +08:00
Sasha Reid
2e4acba105
Microwavenby dns hostinger (#6843)
* [Microwavenby--dns_hostinger] Adding initial dns support for Hostinger.com

* [Microwavenby--dns_hostinger] Creating a commit now that workflows are enabled

* [Microwavenby--dns_hostinger] Correcting shellcheck. Why is this not automatic?

* [Microwavenby-dns-hostinger] Responding to comments from Neil

* [dns-hostinger] SHfmt and Shellcheck

* [dns-hostinger] Writing non-greedy-ish regexes. correcting copypasta
2026-07-11 10:04:49 +08:00
Mike Lei
fef90e15e1
Fix name.com DNS API for processing IDNs (#4381) 2026-07-10 21:56:54 +08:00
Roman Lumetsberger
50e5e771d5
Feature: Support other shells then sh (#4877)
* Add support for DEPLOY_SSH_REMOTE_SHELL

* allow to configure quoting of  remote cmd string

* shell check and shellfmt fixes
2026-07-10 21:02:06 +08:00
CV
ebde8345ae
dns_ispconfig.sh client_id not numeric at ispconfig v 3.2.7p1 (#4999)
Getting client_id failed due to incorrect extraction!

At least in version 3.2.7p1 and probably later the plugin is not working any more properly. The result of ```curResult="$(_post "${curData}" "${ISPC_Api}?client_get_id")"``` is something like this ```Result of _ISPC_ClientGetID: '[Tue Jan 23 11:44:57 CET 2024] Retrying post 
{"code":"ok","message":"","response":3}[Tue Jan 23 11:44:57 CET 2024] _hcode 0'```. The parsing code does not work properly and leaves a non numeric value such as ```Client ID: '3[Tue Jan 23 11'```.
2026-07-10 20:55:23 +08:00
neil
58cd667d65 dns_njalla.sh: accept string record ids when removing records
The Njalla API returns record ids as JSON strings now; the numeric-only
pattern matched nothing, so the removal never found the record id.
Match both quoted and bare ids.

from https://github.com/acmesh-official/acme.sh/pull/5121
2026-07-10 20:47:31 +08:00
Andrew V.
98c30912fb
2024-12-24 - Ensure that $PDNS_Url has no trailing slash ('/'). (#6171) 2026-07-10 20:36:42 +08:00
neil
2058a77d83 acme.sh: fix variable name typo _excapedAlgnames -> _escapedAltnames
from https://github.com/acmesh-official/acme.sh/pull/6547
2026-07-10 19:37:36 +08:00
neil
660a5e322c deploy/synology_dsm.sh: use grep -Eo '[0-9]+' when extracting error codes
grep -o '[0-9]*' can match the empty string; GNU grep skips empty
matches but BSD greps handle them differently, breaking the 2FA
login flow on OpenBSD. Force a non-empty match at all three sites.

from https://github.com/acmesh-official/acme.sh/pull/6725
2026-07-10 18:37:32 +08:00
Jan Pieper
2af543a358
Fix typo (#6924) 2026-07-10 14:42:57 +08:00
neil
fa763db105 dns_pleskxml.sh: use grep -F when matching interpolated values
Some checks are pending
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Nginx / Nginx (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
fulldomain/txtvalue/root_domain_name were interpolated into grep
regex patterns; match them as fixed strings instead.

from https://github.com/acmesh-official/acme.sh/pull/7031
2026-07-10 12:05:28 +08:00
neil
90b4795bb1 issue: strip the trailing dot of a fully-qualified alias domain
A trailing dot in --domain-alias/--challenge-alias was passed through
to the dnsapi hook verbatim. Providers with exact-match record-name
lookups (e.g. Cloudflare's name= filter) then never find the record,
so rm never deletes it and relic TXT records accumulate on every issue.
Stripping in issue() also fixes certs with a dotted alias already
saved in domain.conf.

fix https://github.com/acmesh-official/acme.sh/issues/4636
2026-07-10 11:22:20 +08:00
neil
534a1714dc dns_me.sh: use LC_ALL=C so the request date header is always English
LC_ALL in the environment overrides both LC_TIME and LANG, so LANG=C
alone still produced localized day/month names on non-English systems
and DNS Made Easy rejected the request date header. An LC_ALL=C
command prefix beats every locale variable (same pattern as
dns_oci.sh).

Fixes #4272. Closes #4271. Thanks to @Nickinthebox.
2026-07-10 11:20:08 +08:00
neil
45c0ad4112 Add _cleardeployconf to clear deploy hook keys from domain conf
Mirrors _clearaccountconf_mutable: clears the SAVED_ prefixed key and
the legacy unprefixed key. Replaces the local copy in synology_dsm.sh
and the direct _cleardomainconf call in multideploy.sh.

Closes #4722. Thanks to @sg1888.
2026-07-10 11:05:40 +08:00
neil
83b52e0cd7 notify/smtp.sh: add --crlf so curl sends CRLF line endings
Postfix with smtpd_forbid_bare_newline (default hardening since 3.9,
after SMTP smuggling) rejects the message with
"521 5.5.2 Error: bare <LF> received". RFC 5321 requires CRLF.
The python sender is unaffected (smtplib already emits CRLF).

fix https://github.com/acmesh-official/acme.sh/issues/7104
2026-07-10 10:38:52 +08:00
neil
bed15ba844 dns_freedns.sh: use grep -E, BRE \| alternation is a GNU extension
OpenBSD grep treats \| in a BRE as a literal | character, so
_freedns_domain_id never matched any row and every domain lookup
failed with "Domain not found". Switch to ERE with -E, keeping the
parens escaped so the (.*) suffix branch still requires literal
parentheses and does not widen the match (e.g. searching example.com
must not match example.company).

Reported-by: @katiekloss @boretom
Ref: https://github.com/acmesh-official/acme.sh/issues/2305
2026-07-10 10:23:59 +08:00
invario
1324dcd472
Docker: update crontab used (#7111)
Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2026-07-10 10:10:44 +08:00
lwohn-creo
cf3eab95ee
Add creoline API as DNS provider (#7100)
* New Banner

Updated README to include responsive images for dark and light modes.

* acme-sh-creoline-as-dns-provider

* acme-sh-creoline-as-dns-provider - Review changes implemented according code review

* acme-sh-creoline-as-dns-provider - Review changes implemented according second code review, minding --cron

* acme-sh-creoline-as-dns-provider - Remove debug code

* acme-sh-creoline-as-dns-provider - shfmt formatting according Code of conduct

---------

Co-authored-by: neil <github@neilpang.com>
Co-authored-by: ZeroSSL-Andreas <andreas.schuster@hidglobal.com>
Co-authored-by: Steven Kauschke <s.kauschke@creoline.com>
2026-07-10 10:07:12 +08:00
neil
ca118be754 issue.yml: match tracking issue title variants 2026-07-10 10:02:59 +08:00
Marvo2011
b1b539695f
Merge pull request #7026 from Marvo2011/dev
Some checks failed
DNS / CheckToken (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
Update SelfHost DNS provider
2026-07-06 21:30:13 +08:00
neil
347cc207cd
Merge pull request #7096 from acmesh-official/dev
one-click revert and ban
2026-07-06 17:40:51 +08:00
neil
cc64a73230 one-click revert and ban 2026-07-06 17:39:56 +08:00
neil
1651a5a609
Merge pull request #7095 from acmesh-official/dev
sync
2026-07-06 16:37:03 +08:00
neil
f2b37b32ff add more events 2026-07-06 16:36:16 +08:00
neil
73df21abc4 clean 2026-07-06 16:30:41 +08:00
neil
0c76c1f211
Merge pull request #7094 from acmesh-official/dev
wiki-guard: use WIKI_GUARD_TOKEN (PAT with read:org) to enumerate org…
2026-07-06 16:28:12 +08:00
neil
919492df13 wiki-guard: use WIKI_GUARD_TOKEN (PAT with read:org) to enumerate org write members 2026-07-06 16:27:26 +08:00
neil
e00d3cfde3
Merge pull request #7092 from acmesh-official/dev
wiki-guard: log the number of write-access members loaded
2026-07-06 15:24:23 +08:00
neil
e4eaa59063 wiki-guard: log the number of write-access members loaded 2026-07-06 15:23:45 +08:00
neil
4187ec23c1
Merge pull request #7091 from acmesh-official/dev
wiki-guard: trust repo/org members with write access in all rule checks
2026-07-06 15:12:22 +08:00
neil
2b5a19d34a wiki-guard: trust repo/org members with write access in all rule checks 2026-07-06 15:11:35 +08:00
neil
9f0ef7abcd
Merge pull request #7090 from acmesh-official/dev
sync
2026-07-06 14:28:08 +08:00
neil
dc1b06006f issue.yml: assign and label "Report bugs to" tracking issues instead of posting the upgrade boilerplate 2026-07-06 14:27:00 +08:00
neil
8585d9f4a7 wiki-monitor: skip notification for the maintainer's own wiki changes 2026-07-06 14:23:51 +08:00
neil
0e2659b768
Merge pull request #7088 from acmesh-official/dev
add wiki-guard workflow: auto-restore wiki pages deleted or renamed b…
2026-07-06 13:37:00 +08:00
neil
ca8ab7f8b5 add wiki-guard workflow: auto-restore wiki pages deleted or renamed by non-maintainer 2026-07-06 13:35:37 +08:00
neil
f50401a342
Merge pull request #7086 from acmesh-official/dev
Some checks failed
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
sync
2026-07-06 10:37:29 +08:00
Simon V.
ff9b969bdb
Add support for Account Key Rollover (#7080)
Some checks failed
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
* add wiki

* feat: add support for account key rollover

* Place --update-account-key next to --update-account

* fix shfmt

* fix shfmt

* fix shfmt

* Fix from review

* fix shfmt

* fix from review

* fix review

---------

Co-authored-by: neil <gitpc@neilpang.com>
2026-07-06 10:22:36 +08:00
Oliver Mueller
1f778e6ef1
deploy/ssh: return non-zero when a server deployment fails (#6795)
ssh_deploy() ignored the result of _ssh_deploy and always returned
success, so a failed transfer to one (or all) of the servers in
DEPLOY_SSH_SERVER was silently swallowed. Track the return code across
the loop and return non-zero if any server failed, letting the caller
handle notification.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 10:11:58 +08:00
PM Extra
58423df3e8
retry failed install and deploy on renew (#7083)
* retry failed install and deploy on renew

* fix notify level for renew retry failures
2026-07-05 23:33:57 +08:00
LaoDC
8f3c1701f3
Add LaoDC DNS API (dns_laodc) (#6974)
* Added LaoDC API Module

* Cleaned up debug and info
revised get subdomain to filter by TXT records.

* Added commet to _get_root

* Removed PATCH logic of updating acme records as this doesn't work for wildcard DNS.

Revised rm() function to do explicit record matching.

* Revised _get_root() to handle different scenarios.

Fixed _laodc_api() function to check if query failed to run.
added basic json sanitation to handle \ and " in $value
unset _H2 _H3 after call as per request from copilot.

* fixed indentation of case statement block

* fixed condition checking.

_get_root should start at 1 so full fqdn can be tested
$? was being reference after export command failing response checks
removed export txtvalue
fixed docs link and issues link

* Verify key for both add and rm

* fixed dns alias condition check
validate key returns 1 if failed.

---------

Co-authored-by: neil <github@neilpang.com>
Co-authored-by: LaoDC <git@laodc.com>
2026-07-05 23:30:26 +08:00
neil
2a175f97e8 toPkcs8: support --password and re-export the pkcs8 file on renewal (#4134) 2026-07-05 18:04:24 +08:00
neil
d3af3315da dnsapi/dns_edgedns: use the system clock for the request timestamp (#3973) 2026-07-05 18:02:34 +08:00
neil
7b6d96387c migrate the legacy ACMEDNS_UPDATE_URL from the account conf (#3899) 2026-07-05 17:48:33 +08:00
neil
cabe432539 add bash completion for commands and parameters, installed via --install (#307) 2026-07-05 17:39:50 +08:00
neil
1746fbdb25 support multiple account emails (#1309)
ACCOUNT_EMAIL / --email now accepts a comma- or space-separated list
and registers all of them as ACME contact entries. The ZeroSSL EAB
endpoint takes a single address, so the first one is used there.
2026-07-05 17:37:40 +08:00
neil
d2b3772631 deploy/panos: do not commit when the cert or key import failed (#4716)
Committing after a failed import leaves a mismatched cert/key pair on
the firewall (PAN-OS does not validate the pair at commit time), which
can lock the admin out of the https management interface.
2026-07-05 17:13:59 +08:00
neil
e964157bff _install_win_taskscheduler: zero-pad the minute in the schtasks /ST value (#4950) 2026-07-05 17:13:24 +08:00
neil
934711e51d notify/aws_ses: add container/instance IAM role auth (IMDSv2)
aws_ses_send calls `_use_container_role || _use_instance_role` when no
static AWS keys are set, but those functions were never defined -- only
_use_metadata was -- so role-based auth silently fell through to the
"no api key" error. Add both, using the current IMDSv2-capable versions
from dns_aws.sh, and set the IMDSv2 token header in _use_metadata so the
credential fetch works on IMDSv2-only instances.

Closes #4742
2026-07-05 17:09:51 +08:00
neil
7def43481a dns_regru: require a dot boundary in root zone matching
_get_root matched a registered domain anywhere as a substring of the
challenge domain, so with both "test.com.ru" and "subtest.com.ru" in the
account, issuing for subtest.com.ru wrongly picked test.com.ru as the
root (it is a substring of "sub-test.com.ru"). Anchor the match to a '.'
boundary so a shorter domain no longer matches a longer subdomain label.

Fixes the issue reported in #5036 (thanks @koledas)
Closes #5036
2026-07-05 16:57:59 +08:00
neil
defd64022d dnsapi/dns_namecom: probe the root zone with GetDomain instead of listing all domains
The domain list is paginated at 1000 entries per page and only the
first page was fetched, so accounts with more than 1000 domains never
found the root zone.

fix https://github.com/acmesh-official/acme.sh/issues/5051
2026-07-05 16:53:14 +08:00
neil
4256e3532b _regAccount: error out clearly when the eab-hmac-key cannot be base64-decoded
An undecodable key (e.g. broken LibreSSL base64 -d -A) used to produce
the cryptic "Usage: _hmac hashalg secret [outputhex]" and an empty EAB
signature that the CA rejects with 403.

https://github.com/acmesh-official/acme.sh/issues/4082
2026-07-05 16:32:57 +08:00
neil
1f94fd7fd5 add Apache workflow to test the --apache mode
Runs le_test_apache from acmetest against Pebble, with Apache
listening on Pebble's HTTP-01 validation port.
2026-07-05 16:31:51 +08:00
neil
507baff2ef deploy/docker: allow setting key file mode and owner in the container
The docker deploy hook copied the key file preserving the source mode
(root:root 0600), so a non-root container service (uid >= 1000) could not
read it. Add DEPLOY_DOCKER_CONTAINER_KEY_MODE and
DEPLOY_DOCKER_CONTAINER_KEY_OWNER, applied via chmod/chown inside the
container after the key is copied and before the reload command.

Closes #5333
2026-07-05 16:30:46 +08:00
neil
504540e67c dnsapi/dns_autodns: escape XML special characters in credentials (#5317) 2026-07-05 16:29:58 +08:00
neil
cacafc9c23 add Nginx workflow to test the --nginx mode
Runs le_test_nginx from acmetest against Pebble: nginx listens on
Pebble's HTTP-01 validation port with an aaPanel/BT style
"location ^~ /" reverse proxy block, the regression case of #6125.
2026-07-05 16:18:37 +08:00
laineus
24895a15c8
Add dns_muumuu: muumuu-domain.com DNS API (#7012)
* Add dns_muumuu: muumuu-domain.com DNS API

* Fix: remove local keyword for POSIX sh compatibility

* Fix: lowercase fulldomain for API compatibility

* Style: use echo instead of printf for lower_case (consistent with other plugins)

* Fix: prefix rest vars, clear _H4/_H5, guard record_id, update Issues URL
2026-07-05 16:05:33 +08:00
neil
1cd63e1480 _createcsr: omit CN from the CSR subject when it exceeds 64 characters (#4867) 2026-07-05 16:02:52 +08:00
neil
31b13caf8b DNS.yml: fix workflow warnings
- replace deprecated set-output with GITHUB_OUTPUT
- untap aws/tap before brew install to silence tap trust warning
- inject safe.directory=* for cygwin git so the checkout post step
  no longer fails with dubious ownership (exit 128)
2026-07-05 15:51:38 +08:00
neil
1e2cd50fc9 deploy/haproxy: use printf instead of "echo -e" for the stats socket payload
dash's echo has no -e flag and sends a literal "-e " prefix to the
socket, so haproxy rejects the command and the hot update always fails
on Debian/Ubuntu (/bin/sh = dash). Also accept "Transaction updated",
which haproxy replies when an uncommitted transaction already exists.

fix https://github.com/acmesh-official/acme.sh/issues/6165
2026-07-05 15:44:26 +08:00
Jan Forman
524d96a3a8
Add WEDOS WAPI DNS API (dns_wedos) (#7072)
* Add WEDOS WAPI DNS API (dns_wedos)

* dns_wedos: fix response parsing on systems without egrep -o

* dns_wedos: report WAPI auth errors, UTC fallback for hosts ignoring TZ
2026-07-05 12:44:07 +08:00
Foster Snowhill
0eb5cc8384
dns_desec: fix advertised token variable name (#7081)
Some checks are pending
Linux / Linux (gentoo/stage3) (push) Waiting to run
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
This must've been a copy-paste error from `dns_ddnss`.

Fixes: 6b7b5caf54 ("DNS provider API: structured description")
2026-07-05 12:03:48 +08:00
neil
77047eb0ef fix CSR reading on systems without a default openssl.cnf (e.g. NetBSD)
"openssl req -noout -in" aborts when the default config file is missing;
reading a CSR needs no config, so pass -config /dev/null explicitly.

Stock NetBSD does not install /etc/openssl/openssl.cnf, so --signcsr
never worked there.
2026-07-04 23:55:16 +08:00
neil
4978782fb8 renewAll: error out if CERT_HOME is not a directory
With a misconfigured $HOME / CERT_HOME the glob over "$CERT_HOME"/*.*
matches nothing, so renewAll silently does nothing and returns success --
--renew-all / --cron appears to work while renewing no certificates.
Check that CERT_HOME is a directory up front and return 1 with a clear
error instead.

Closes #4508
2026-07-04 23:40:31 +08:00
neil
9764f67619 dns_cn: convert IDN domain to punycode before API calls
Core-Networks' API rejects Unicode domain names with "invalid domain";
it requires punycode. dns_cn_add / dns_cn_rm passed the raw challenge
domain straight through, so IDN certs failed at the TXT add step
(issue #4804). Run fulldomain through _idn() in both functions. For
ASCII/punycode input _idn() is a pass-through, so non-IDN domains are
unaffected.

Fixes #4804
2026-07-04 21:58:57 +08:00
neil
988afd0f59 _isIPv4: do not glob segments, require exactly 4 octets
The unquoted splitting let a "*" segment expand against files in the
current directory, so "*.*.*.*" could pass as a valid IPv4 address
(issue 4971). The old code also accepted "", "1.2.3", "1.2.3.4.5",
"1..2.3" and bare numbers. Split with IFS under set -f, require 4
octets, and validate each as a 1-3 digit number <= 255.

Based on https://github.com/acmesh-official/acme.sh/pull/4974
fix https://github.com/acmesh-official/acme.sh/issues/4971
2026-07-04 21:34:46 +08:00
Ramon
b92516f79e
add application/json to acmedns (#5066) 2026-07-04 21:07:18 +08:00
neil
fbf3b41c54 dns_inwx: fix _get_root false zone match for single-letter subdomains
_get_root matched the candidate zone with _contains (grep), which treats
the domain as a regex. For "-d g.<zone>" the candidate "g.<zone>" matched
"<string>...<zone>" because '.' matches the '>' after "string" and the 'g'
comes from the "<string>" tag, so "g.<zone>" was wrongly taken as the root
zone (sub=_acme-challenge instead of _acme-challenge.g). Anchor the match
to <string>$h</string> and escape dots so the zone is compared literally.

Fixes #5129
2026-07-04 20:28:56 +08:00
neil
ede9a86d46 Accept both 401 and 403 for deactivated account detection
RFC 8555 sec 7.3.6 requires 401 (Unauthorized) when a request is
signed by a deactivated account, which ZeroSSL follows, while
Boulder (Let's Encrypt) historically returns 403. Check both codes
in _regAccount and deactivateaccount.

fix https://github.com/acmesh-official/acme.sh/issues/5138
2026-07-04 20:21:27 +08:00
xiaopc
843a7efa7d
fix(gcore_cdn): renew login api url (#5143)
https://api.gcore.com/docs/iam#tag/Account
2026-07-04 20:14:32 +08:00
neil
d3e12694b9 fix "identifiers are duplicated" when signing a CSR with a wildcard CN also present in SAN
_contains matches with grep regex, so the '*' in "DNS:*.example.com," never
matched and the subject was appended to the identifiers a second time.
Escape the wildcard before the check, the same way the sed removal already does.

fix https://github.com/acmesh-official/acme.sh/issues/5251
2026-07-04 19:56:37 +08:00
neil
a105126063 _date2time: pass date via argv to python to prevent code injection (#6463)
https://github.com/acmesh-official/acme.sh/issues/6463
2026-07-04 19:14:36 +08:00
neil
917bebd460 dns_huaweicloud: add optional HUAWEICLOUD_Region (default ap-southeast-1)
The DNS endpoint and IAM token scope project were hardcoded to
ap-southeast-1, which fails for accounts without that region enabled.

fix https://github.com/acmesh-official/acme.sh/issues/5302
2026-07-04 19:06:46 +08:00
Clément Gouin
33704fc274
Allow creation of ACME account with EAB directly from --issue command (#5087)
* formalized _eab_id and _eab_kid and added EAB parameters to _regAccount on --issue

* Update acme.sh

* Update acme.sh
2026-07-04 18:54:07 +08:00
neil
bcbfe25d08 haproxy.sh: use two-argument -header form for LibreSSL (#3438) 2026-07-04 18:50:29 +08:00
neil
bbfb6f50ae deploy/cpanel_uapi: strip YAML double quotes around wildcard domains in list_domains output
fix https://github.com/acmesh-official/acme.sh/issues/6115
2026-07-04 18:46:59 +08:00
neil
6df2d9e451 dns_da: document that special characters in DA_Api credentials must be percent-encoded
https://github.com/acmesh-official/acme.sh/issues/3468
2026-07-04 18:19:45 +08:00
neil
1a36823461 https://github.com/acmesh-official/acme.sh/issues/3201 2026-07-04 16:55:49 +08:00
wardhus
8f2a476d21
Add Calrissia.be API (#6811)
Co-authored-by: Ward <ward.hus@calrissia.com>
2026-07-04 16:54:05 +08:00
Simon V.
e64529ab50
ARI - Add support for Mass Revocation (#6953)
* ARI - Add support for Mass Revocation

* feat: update ARI each time NextRenewTime is not within the suggestedWindow

* Remove _ari_should_renew and add condition on Le_NextRenewTime

* Add support for ARI explanationURL

* Fix debug variable _d_ari

* New Banner

Updated README to include responsive images for dark and light modes.

* multiple fix

* fix

* fix shfmt

* Reset README

---------

Co-authored-by: ZeroSSL-Andreas <andreas.schuster@hidglobal.com>
2026-07-04 16:50:55 +08:00
neil
7653eaab31 fix https://github.com/acmesh-official/acme.sh/issues/4879#issuecomment-2942728895
Some checks are pending
Linux / Linux (gentoo/stage3) (push) Waiting to run
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
2026-07-04 11:44:32 +08:00
neil
6cd0c00a21 extract authorizations parsing into _authorizations_from_order, fix IPv6 urls (#6326) 2026-07-04 11:23:11 +08:00
neil
b4de9e8621 fix docker deploy hook on podman, check exec ExitCode instead of response body (#4977) 2026-07-04 10:49:32 +08:00
neil
0a6abaf8a1 fix https://github.com/acmesh-official/acme.sh/issues/6388 2026-07-04 00:39:52 +08:00
Laurent Grawet
f4dc9fd9d1
haproxy.sh: allows certificate deployment to multiple hosts (#5180)
* haproxy.sh: allows certificate deployment to multiple hosts

* Update deploy/haproxy.sh

Co-authored-by: Matt Simerson <matt@tnpi.net>

* Update deploy/haproxy.sh

Co-authored-by: Matt Simerson <matt@tnpi.net>

---------

Co-authored-by: Matt Simerson <matt@tnpi.net>
2026-07-04 00:35:02 +08:00
Artur Klauser
2229330c48
Fix typo in synology_dsm.sh (#6406)
Fix typo in an error message.
2026-07-04 00:32:45 +08:00
neil
0df051577c fix https://github.com/acmesh-official/acme.sh/issues/6609 2026-07-04 00:06:26 +08:00
neil
7fb40f0ccf fix https://github.com/acmesh-official/acme.sh/issues/6609 2026-07-03 23:55:23 +08:00
neil
780f2ad5dc dns_ali: do not rely on "_url_encode upper-hex" so the signature works with older bundled libraries (e.g. Proxmox VE) https://github.com/acmesh-official/acme.sh/issues/6272 2026-07-03 23:44:22 +08:00
szakharchenko
adf69c4e7e
dns_aws: Fix invalid domain logging: _error => _err (#6430)
acme.sh defines _err, not _error.
2026-07-03 23:40:39 +08:00
neil
ac5624536b dns_gd: fix root zone detection for API-restricted accounts https://github.com/acmesh-official/acme.sh/issues/4487 2026-07-03 23:34:27 +08:00
neil
b974bbd6d6 fix upgrade with a relative --home path https://github.com/acmesh-official/acme.sh/issues/6477 2026-07-03 23:34:27 +08:00
szakharchenko
0ce8c24736
dev_mythic_beasts: Fix header name: Accepts => Accept (#6428) 2026-07-03 23:25:38 +08:00
neil
20254cbaf0 dns_dnsimple: support user tokens (dnsimple_u_*) https://github.com/acmesh-official/acme.sh/issues/6491 2026-07-03 23:02:35 +08:00
neil
f4d2db64ef dns_gd: skip readback check when GoDaddy API returns UNKNOWN_DOMAIN https://github.com/acmesh-official/acme.sh/issues/6517 2026-07-03 22:49:40 +08:00
Trekky12
61400500e2
Suppress 'signal process started' message when nginx config is restored (related to issue #4995) (#6747) 2026-07-03 22:31:31 +08:00
neil
92a1b47108 forbid spaces in the --home/--config-home path https://github.com/acmesh-official/acme.sh/issues/2163 2026-07-03 22:26:50 +08:00
neil
5038d12d62 forbid using --days together with --valid-to https://github.com/acmesh-official/acme.sh/pull/6572 2026-07-03 21:55:37 +08:00
magyarsz
7e7c0ee984
Merge pull request #6720 from magyarsz/dev
Fix a logical error in the `renew` function
2026-07-03 21:42:52 +08:00
neil
92bd80c07d fix https://github.com/acmesh-official/acme.sh/issues/6914 2026-07-03 20:07:42 +08:00
neil
ad99628e50 fix https://github.com/acmesh-official/acme.sh/issues/6917 2026-07-03 20:04:32 +08:00
neil
992886c4eb
Merge pull request #7078 from acmesh-official/dev
Some checks failed
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
sync
2026-07-03 19:56:09 +08:00
neil
eabd23a551 fix https://github.com/acmesh-official/acme.sh/issues/6963 2026-07-03 19:38:42 +08:00
neil
1241649501 fix https://github.com/acmesh-official/acme.sh/issues/7009 2026-07-03 19:38:21 +08:00
neil
9900adb007 fix https://github.com/acmesh-official/acme.sh/issues/4756 2026-07-03 19:01:10 +08:00
ZeroSSL-Andreas
01d6d46914 New Banner
Updated README to include responsive images for dark and light modes.
2026-07-03 18:33:44 +08:00
CZECHIA-COM
f038958192
fix(dns_czechia): read _normalizeJson input from stdin, not as an argument (#7077)
_normalizeJson reads its JSON from stdin (sed | sed | tr) and ignores
any positional argument. dns_czechia_add() called it as
`_normalizeJson "$_res"`, so the response was discarded and the inner
sed blocked reading from stdin.

When issuing for a single domain, or for a record that already exists,
the "already exists" branch returns early and never reaches this call,
which is why the bug stayed hidden. With multiple domains, the first
record often short-circuits on "already exists" while the next,
freshly-added record reaches the broken call and hangs on interactive
runs (or consumes unrelated stdin non-interactively).

Pipe the response into _normalizeJson via stdin, matching
dns_czechia_rm() and every other dnsapi plugin.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:12:25 +08:00
Howtin
fdf528c26c
feat: Add Volcano Engine DNS API (#7069)
* feat: add volcengine dns api

* fix(volcengine): address review findings and fix record matching

Code review fixes:
- fix format string usage in signature computation (use printf %b / %s)
- clear _H1.._H5 header state at start of request to avoid leaking
  conditionally-set headers into subsequent requests
- check ListZones return status in _get_root
- document Volcengine_SESSION_TOKEN option and fix duplicate "and" typo
- fix Docs and Issues sections
- remove and update some code comments

Functional fixes:
- stop matching ListRecords results by FQDN string: Volcengine lowercases
  the Host/FQDN in responses, so a case-sensitive compare against
  $fulldomain failed for mixed-case names, making rm silently skip
  deletion and add lose idempotency. ListRecords is already filtered by
  ZID+Host+Value+SearchMode:exact, so just extract RecordID from the
  result instead.
- reset _record_id at the start of add/rm to avoid stale state leaking
  across calls within the same process
- tag created records with Remark "acme.sh" for easier identification
- adjust debug levels: hide Authorization header behind _debug2, surface
  response at _debug

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: wenxuan70 <t736660416@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:09:32 +00:00
Simon V.
ced8d72808
ARI - Run cron job more frequently (#6939)
* ARI - Run cron job more frequently

With ACME Renewal Info (RFC9773 §4.3), fetching renewal window should be more frequent, e.g. in case of revocation incident.
"For instance, a server that needs to revoke certificates within 24 hours of notification of a problem might choose to reserve twelve hours for investigation, six hours for clients to fetch updated RenewalInfo objects, and six hours for clients to perform a renewal."

More flexible option is to run the cron job every hour and only refresh ARI when the last one + Retry-After header is in the past.

* Fix cron job schedule for certificate renewal

* Fix random_hour syntax in cron job installation

* Update Windows task scheduler to run more frequently

Add support for randomized hour and update frequency

Ref:
* [/mo](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks-create#to-schedule-a-task-to-run-every-n-hours)
* [/SC HOURLY](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks-create#parameters)

* Add padding for StartTime (/ST) in SCHTASKS.exe

* New Banner

Updated README to include responsive images for dark and light modes.

* rebase

* Reset README

---------

Co-authored-by: ZeroSSL-Andreas <andreas.schuster@hidglobal.com>
2026-07-03 15:12:32 +08:00
neil
116c05fbff fix https://github.com/acmesh-official/acme.sh/issues/6400
Some checks are pending
Linux / Linux (gentoo/stage3) (push) Waiting to run
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
2026-07-02 23:30:56 +08:00
neil
42e13fa797 fix https://github.com/acmesh-official/acme.sh/issues/6851 2026-07-02 23:18:26 +08:00
neil
ccd2f04c33 fix https://github.com/acmesh-official/acme.sh/issues/6986 2026-07-02 23:15:41 +08:00
MarFri
a50fad865f
DNS_IONOS double sending content type & case sensitive mismatch (#7028)
* double sending content type results in error from ionos

* Normalize fulldomain to lowercase in the _ionos_get_record function.
2026-07-02 23:08:59 +08:00
Toni Karppi
5433ea86c8
Add Glesys dnsapi provider (#7059)
* Add Glesys dnsapi provider

* Fix typo in error message for dns_glesys_add

* Use API to get record id in Glesys provider

* Use listrecords API endpoint to find root domain

* Remove record id parsin from add function
2026-07-02 13:10:29 +08:00
neil
e52d75d762 fix https://github.com/acmesh-official/acme.sh/issues/7062 2026-07-02 13:09:59 +08:00
Manwe Sulimo
cb40b47a4e
Implemented support for Aruba Business DNS API (#7042)
* Add support for arubabusiness api

* Fix formatting

* record names are always converted to lowercase

* Docs

* remove leftover unconditional authentication call
lowercase
urlencoded body + x-www-form-urlencoded content-type
cleanup header variables
cleanup typos
grammar

* Strengthen _ab_rest failure checks
Properly process parallel lists in _ab_dns_record_id
Remove hard fails when a txt record already exists

* fix json parsing

* Fix formatting

---------

Co-authored-by: Manwe-Sulimo <blarghonaut@gmail.com>
2026-07-02 13:06:41 +08:00
pxMan79
b039ff3087
fix(dns_baidu): prefer new Baidu DNS API with legacy BCD fallback (#6992)
* fix(acme): prefer new Baidu DNS API with legacy BCD fallback

Keep the existing BCD implementation and add fallback support for the newer Baidu DNS record API. Prefer the new API by default, then fall back to the legacy BCD API to reduce compatibility risk.

* fix(dns_baidu): route through _get/_post + restore legacy BCD auth headers

Per review: _baidu_dns_call now uses _get/_post with _H1.._H5 (no raw curl, no __HTTP_STATUS__ parsing); _baidu_bcd_post restores _H1.._H5 so the legacy BCD path sends the Authorization signature again (fixes 401).

---------

Co-authored-by: neil <github@neilpang.com>
2026-07-02 13:02:30 +08:00
bluenenschloss
c83eed4994
dns_inwx: fix IDN zone detection without python dependency (#7056)
* dns_inwx: fix IDN zone detection without python dependency

INWX returns zone names in Unicode form (e.g. lünenschloß.de) even when
the domain was registered as an IDN. When acme.sh passes the SAN in
punycode (xn--lnenschlo-o1a42a.de), _contains never matches and
_get_root falls through to the TLD, placing the TXT record in the wrong
zone.

Previous fix used python3 which is not available in all environments
(BusyBox, BSD, minimal containers). Replace with _idn()-based approach:
extract <string> values from the nameserver.list XML response, encode
each via _idn(), and compare to $h. When a match is found, use the
original Unicode zone name for createRecord.

Fixes #7038

* dns_inwx: fix shebang, use _egrep_o, shfmt cleanup

- Revert shebang to #!/usr/bin/env sh (POSIX sh, fixes ShellCheck)
- Replace grep -o with _egrep_o for portability
- shfmt -i 2: drop backslash continuation after pipe, fix indentation

Requested by @neilpang

* fix: drop closing </string> from _egrep_o pattern to avoid sed delimiter collision

---------

Co-authored-by: bluenenschloss <bernd.luenenschloss@7p-group.com>
2026-07-02 13:00:49 +08:00
neil
81100db2f3 fix https://github.com/acmesh-official/acme.sh/issues/6498
Some checks are pending
Linux / Linux (gentoo/stage3) (push) Waiting to run
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
2026-07-01 21:59:41 +08:00
neil
a7ccfcf91d fix 2026-07-01 21:33:24 +08:00
neil
d0fcafe29b fix https://github.com/acmesh-official/acme.sh/issues/7035 2026-07-01 21:22:36 +08:00
Jonas Zürcher
ae8ef74c4b
fix(dns_infomaniak): correctly detect API errors (#7048)
The add/rm success check never rejected anything: for any non-empty API
response it always reported "Record added"/"Record deleted" and returned
0, so the _err branch was dead code. A valid key looked fine only because
the API call genuinely created the record; an invalid key returning
{"result":"error"} produced the same "Record added" output even though
nothing was created.

Root cause, in:
  if [ -n "$response" ]; then
    if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then

  - _contains() ignores stdin (it reads only $1 and $2), so the piped
    "$response" was discarded.
  - The pattern '"result":"success"' was passed as $1 (the haystack),
    leaving $2 (the needle) empty, so it ran:
      echo '"result":"success"' | grep -- "" >/dev/null 2>&1
    grep with an empty pattern always matches.
  - That grep output is redirected to /dev/null, so the command
    substitution always captured "", making [ ! "" ] always true.

Fix: call _contains "$response" '"result":"success"' directly and branch
on its exit code, so error responses now correctly fail (return 1).

Co-authored-by: neil <github@neilpang.com>
2026-07-01 21:05:07 +08:00
SirNomad49
0925d5c522
Adding custom Port definitions for truenas (#7033)
* closing bracket and adding port for customer installations

* adding savedeployconfig

* fixing shfmt

* changeing

---------

Co-authored-by: neil <github@neilpang.com>
2026-07-01 20:55:55 +08:00
hostup
0d53d29f7e
Update dns_hostup.sh to v2 API (#7014)
* Update dns_hostup.sh

Update to v2 api support; developer.hostup.se

* Update dns_hostup.sh
2026-07-01 20:44:21 +08:00
neil
c38182897d fix ghostbsd 2026-07-01 18:59:14 +08:00
Alexander Stehlik
2998106bd1
fix(dns_desec): fix rate limit and compatibility issues (#7027)
* fix(dns_desec): sleep after DNS record change to prevent rate limit issues

Also: make sure the subname is lowercase to fix tests where
the acmetestXyzRandomName subdomain is used.

* fix: make regexes POSIX-compatible (for OpenBSD)

* chore: use _sleep instead of sleep to follow acme.sh standards
2026-07-01 18:54:24 +08:00
Jeroen Moors
b3579ff18d
Implement support for DNS Level27 (#7043)
Some checks failed
DNS / CheckToken (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
* Add Level27 DNS API support

Implements dns_level27_add and dns_level27_rm for the Level27 (level27.eu) DNS API, used for ACME dns-01 challenges.

- Authenticates with a persistent API key via the Authorization header.
- Resolves the registered zone with domains?filter and exact fullname match (supports DNS alias mode).
- Removes the challenge record by its exact TXT value, leaving other records intact (wildcard-safe).
- Optional LEVEL27_API override for non-default/staging endpoints.

* A little better documentation

---------

Co-authored-by: Jeroen Moors <jeroen.moors@level27.be>
2026-06-29 01:27:41 +08:00
Jakub Kołodziejczak
0dc97187e1
docs: introduce contributing doc (#7052)
prevents friction and frustrations like in issue #7050

closes #7050
2026-06-29 00:41:15 +08:00
ZeroSSL-Andreas
d60c75b4e3
New Banner
Updated README to include responsive images for dark and light modes.
2026-06-19 17:06:57 +02:00
regisvidal-bitmapz
365d2d10f3
Fix dns_namesilo_rm failing to remove TXT record (#6969)
Some checks failed
DNS / CheckToken (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
* Fixes #6907
2026-06-19 14:24:20 +02:00
SpeedGriffon
db098055de
Fix RouterOS deploy (#7034)
* routeros: save ROUTER_OS_ADDITIONAL_SERVICES as base64

* routeros: remove cer_3
2026-06-19 14:23:03 +02:00
neil
4575877d48 add GhostBSD
Some checks failed
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
2026-06-05 23:01:42 +02:00
Bill
d98fa53f62
Updated AWS Route53 service endpoint to the dual-stack endpoint (#6994)
* Update to dual-stack service endpoint
2026-06-05 22:06:49 +02:00
aitor422
9b597b3f1b
Add CDMON Api (#6984)
* Added CDMon DNS API
2026-06-05 22:02:14 +02:00
rajcz
58d9c8d7f6
acme.sh: validate cert response before writing .cer (#7006) 2026-06-05 19:38:50 +02:00
Adrian Fedoreanu
a2f046306e
dns_1984hosting: cleanup, memoize zone id (#6978)
* dns_1984hosting: cleanup, memoize zone id, optional OTP
2026-06-05 19:28:08 +02:00
neil
b463471951 fix localaddress
https://github.com/acmesh-official/acme.sh/issues/7009#issuecomment-4633681701
2026-06-05 19:22:25 +02:00
neil
0e5f1518aa upgrade
Some checks are pending
Linux / Linux (gentoo/stage3) (push) Waiting to run
Linux / Linux (kalilinux/kali) (push) Waiting to run
Linux / Linux (opensuse/leap:latest) (push) Waiting to run
Linux / Linux (oraclelinux:8) (push) Waiting to run
Linux / Linux (ubuntu:latest) (push) Waiting to run
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
PebbleStrict / PebbleStrict (push) Waiting to run
PebbleStrict / PebbleStrict_IPCert (push) Waiting to run
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Waiting to run
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Waiting to run
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Waiting to run
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Waiting to run
Build DockerHub / CheckToken (push) Waiting to run
Build DockerHub / build (push) Blocked by required conditions
Shellcheck / ShellCheck (push) Waiting to run
Shellcheck / shfmt (push) Waiting to run
2026-06-04 22:57:50 +02:00
neil
3c848adbb7
Merge pull request #7008 from acmesh-official/dev
Some checks failed
Linux / Linux (gentoo/stage3) (push) Has been cancelled
Linux / Linux (kalilinux/kali) (push) Has been cancelled
Linux / Linux (opensuse/leap:latest) (push) Has been cancelled
Linux / Linux (oraclelinux:8) (push) Has been cancelled
Linux / Linux (ubuntu:latest) (push) Has been cancelled
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
PebbleStrict / PebbleStrict (push) Has been cancelled
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
sync
2026-06-04 22:50:25 +02:00
neil
2e4e5d7955 add tribblix 2026-06-04 22:34:36 +02:00
terafin
c7c903fba3
ci: add GitHub Container Registry (ghcr.io) publishing (#7005) 2026-06-04 21:25:38 +02:00
Thomas
dfbe2c5bff
fix _getAKI() on OpenBSD (#7007)
The order of the arguments does matter for OpenBSD's grep (bug or
feature).
2026-06-04 21:15:12 +02:00
neil
5713c1d39d remove dns_hetzner.sh
Some checks failed
Linux / Linux (debian:latest) (push) Has been cancelled
Linux / Linux (fedora:latest) (push) Has been cancelled
Linux / Linux (gentoo/stage3) (push) Has been cancelled
Linux / Linux (kalilinux/kali) (push) Has been cancelled
Linux / Linux (opensuse/leap:latest) (push) Has been cancelled
Linux / Linux (oraclelinux:8) (push) Has been cancelled
Linux / Linux (ubuntu:latest) (push) Has been cancelled
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
PebbleStrict / PebbleStrict (push) Has been cancelled
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
https://github.com/acmesh-official/acme.sh/issues/6990#issuecomment-4576551997
2026-05-30 11:48:06 +02:00
neil
b7e9214e2d minor 2026-05-30 11:47:25 +02:00
Jakob Næss
206f4494ac
Add Poweradmin DNS API plugin (dns_poweradmin) (#6943)
* Add Poweradmin DNS API plugin (dns_poweradmin)
2026-05-24 22:34:56 +02:00
Simon V.
d9ce7fefa1
ARI - Add support for switching ACME Server during renewal (#6983)
* ARI - Add support for switching ACME Server during renewal

https://github.com/acmesh-official/acme.sh/issues/6964

* Restore old condition while adding malformed
2026-05-24 22:25:52 +02:00
Markus Ebner
ce07759ced
[dnsapi] add IP-Projects dns hook (#6959) 2026-05-24 22:24:00 +02:00
Tom Sommer
010bd1111a
Improve Simply.com API (#6933)
Improve Simply.com API (#6933)
2026-05-24 22:20:40 +02:00
neil
f265c304da
Merge pull request #6968 from acmesh-official/dev
sync
2026-05-16 11:17:24 +02:00
hebbet
85408bfb4d Update renewal condition in acme.sh script 2026-05-16 11:16:20 +02:00
neil
7c91ee1fc0
Merge pull request #6949 from acmesh-official/dev
fix format
2026-05-06 20:59:36 +02:00
neil
1687cbd5b4 fix format 2026-05-06 20:58:54 +02:00
neil
d2780eb04e
Merge pull request #6948 from acmesh-official/dev
sync
2026-05-06 20:52:11 +02:00
neil
eaf4b62ba9 fix for ari 2026-05-06 20:51:49 +02:00
neil
47378b5630 start 3.1.4 2026-05-06 20:51:49 +02:00
neil
ac75c54ade support NO_ARI=1
https://github.com/acmesh-official/acme.sh/discussions/6938
2026-05-06 20:51:49 +02:00
neil
36667ab656 fix ari
https://github.com/acmesh-official/acme.sh/issues/6942#issuecomment-4382355708
2026-05-06 20:51:49 +02:00
neil
7b19070d98 fix ari
https://github.com/acmesh-official/acme.sh/issues/6942#issuecomment-4381535765
2026-05-06 20:51:49 +02:00
neil
7985319687 add wiki 2026-05-06 20:51:49 +02:00
neil
7735cdf3ab
Merge pull request #6937 from acmesh-official/dev
sync
2026-05-02 10:56:54 +02:00
neil
d35876e885 revert 2026-05-02 10:56:13 +02:00
Curd Becker
355b121c79 Add deployment plugin for Windows RDP via OpenSSH (#6925)
* Add deployment plugin for Windows RDP via OpenSSH
2026-05-02 10:56:13 +02:00
Alexander Sulfrian
3230d00c3d Allow renew time relative to the expiration date (#4457)
* Allow renew time relative to the expiration date
2026-05-02 10:56:13 +02:00
neil
fe5490e0ec fix OpenIndiana.yml 2026-05-02 10:56:13 +02:00
neil
0d77231350 add --dns-persist-days 2026-05-02 10:56:13 +02:00
neil
8d5a5a0e0d support ARI by default 2026-05-02 10:56:13 +02:00
neil
cbb8e9068c support dns-persist-01 2026-05-02 10:56:13 +02:00
nsantorelli
96f87844cd Add EuroDNS DNS API plugin (dns_eurodns) (#6903)
Co-authored-by: Nicolas Santorelli <nsantorelli@eurodns.com>
2026-05-02 10:56:13 +02:00
Alexey Pakhomov
8e71268d03 Fix write error $_CRONTAB -l in crontab file (#6920) 2026-05-02 10:56:13 +02:00
firestormisp
10e7e458af Add dns_firestorm.sh plugin for Firestorm.ch DNS API (#6829)
Firestorm.ch is a Swiss hosting provider with managed DNS (PowerDNS).
This plugin allows customers to automate Let's Encrypt DNS-01 challenges
via the Firestorm DNS API.
2026-05-02 10:56:13 +02:00
neil
76d1377fc1
Merge pull request #6934 from acmesh-official/dev
sync
2026-04-28 22:22:24 +02:00
neil
15a0f52577 fix https://github.com/acmesh-official/acme.sh/issues/6930#issuecomment-4327834577 2026-04-28 09:09:50 +02:00
Adam Bodnar
fd6ff7b173
Add dns_cpanel_uapi DNS API plugin (#6878)
* Add dns_cpanel_uapi.sh
2026-04-26 14:29:40 +02:00
neil
ed6d1f3447
Merge pull request #6929 from acmesh-official/dev
sync
2026-04-24 23:30:42 +02:00
neil
0b2187ab3f add MidnightBSD 2026-04-24 22:43:25 +02:00
neil
4b8b23bb90 fix ca name 2026-04-24 21:47:27 +02:00
neil
d00b2722ee
Merge pull request #6889 from achmadalifn4/dev
Add BytePlus ALB deployment script
2026-04-24 21:46:10 +02:00
neil
df49dd2ec9
Merge pull request #6928 from acmesh-official/dev
sync
2026-04-24 21:36:58 +02:00
Achmad Alif Nasrulloh
e9b0cafac5 Fix byteplus_alb.sh 2026-04-24 11:21:43 +07:00
Antoni Company
9882d534af
fix: commit overhaul (#6915)
- Removed scope exclusion for "standard commit".
  - If 'device-and-networks' is excluded (previous behaviour), a certificate for Panorama (always outside of a template) will not be committed (imported to the config but never applied to Panorama). Therefore, panos.sh was only working for certificates used in templates and applied to devices, but not for the Panorama certificate itself.
  - According to the official documentation and the XML API Browser, there is no 'policy-and-objects' that can be excluded.
  - Although it is not mandatory that the user account is solely dedicated to replace certificates and to perform no other type of operations, it is recommended. If such recommendation is applied, the only changes being committed would be in relation to certificates. Therefore, it should be safe not to exclude any scopes.
- Changed the order for "force commit" from '<commit><partial><force>' (unofficial) to '<commit><force><partial>' (official). Both work, but it is recommended to use what is part of the official documentation and/or XML API Browser.
- Removed unofficial 'policy-and-objects' from commented out code (see above).
- Replaced 'exclude' with 'excluded' from commented out code, as per the official documentation. Both work, but see above.
- Replaced 'acmekeytest' with $_panos_user in the commented out code.

Official documentation: https://docs.paloaltonetworks.com/ngfw/api/pan-os-xml-api-request-types-and-actions/commit
XML API Browser: https://<PANOS HOST>/api
2026-04-20 11:28:17 +02:00
neil
539b46adc9 fix https://github.com/acmesh-official/acme.sh/issues/6898#issuecomment-4207794240 2026-04-14 21:19:25 +08:00
ACHMAD ALIF NASRULLOH
f89a9a5de3 Add new header variable _H5 in byteplus_alb.sh
Added a new header variable _H5 to the byteplus_alb.sh script.
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
00090d24b8 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
73a682e561 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
a1b94db94d Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
5c94af86f3 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
3843495397 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
a739bf3e3a Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
Achmad Alif Nasrulloh
75642a1252 Update bteplus_alb.sh 2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
668427f285 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
d0e123cb02 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
8587c3e744 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
934870fc77 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
8eea7ca307 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
28f1f07f49 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
ad71a785ec Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
d5c8060a65 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
4178c33524 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
044371b00a Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
86d98b0461 Update deploy/byteplus_alb.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-14 21:04:48 +08:00
ACHMAD ALIF NASRULLOH
6efd6d5b5a Add BytePlus ALB deployment script
This script deploys SSL/TLS certificates issued by acme.sh to BytePlus Application Load Balancer (ALB), supporting automatic renewal with zero-downtime certificate rotation.
2026-04-14 21:04:48 +08:00
GNDevProd
454cec6e43
Add Gname.com dnsapi support (#6808)
* add gname dns acme.sh
2026-04-14 20:57:22 +08:00
wangzhizhou
afba1455b8
add dnsapi for baidu cloud dns (#6844) 2026-04-14 20:44:30 +08:00
Mitchell van Bijleveld
f3e61a8ef4
Don't mark restart http as failed if json returns false because it was not restarted (#6906) 2026-04-14 20:37:50 +08:00
neil
0894955895 fix https://github.com/acmesh-official/acme.sh/pull/6731#issuecomment-3733144962 2026-04-08 22:33:30 +08:00
brevilo
5b5ef91d88
Fix off-by-one error preventing the final poll to succeed (#6865)
When the final poll (`_link_cert_retry` at 29) returns, the status
is never checked again. So even a `valid` status goes unnoticed.
It's a pre-test loop after all.

Co-authored-by: Oliver Behnke <oliver.behnke@aei.mpg.de>
2026-04-08 22:18:26 +08:00
Jordan Russell
618735d11e
[dnsapi] add SiteHost DNS API hook (#6891)
Co-authored-by: Jordan Russell <jordan.russell@sitehost.co.nz>
2026-04-08 22:07:30 +08:00
Lorenz Stechauner
08b2186afe
dns_world4you: Adapt to latest record id changes (#6897) 2026-04-08 22:00:42 +08:00
Stefan Bottelier
3509f6404f
Add bHosted.nl DNS API (#6864)
Add bHosted.nl DNS API (#6864)
2026-04-08 21:59:09 +08:00
neil
50dbdd781b fix DEFAULT_RENEW
fix https://github.com/acmesh-official/acme.sh/issues/2217#issuecomment-4155894630
2026-04-05 10:46:29 +08:00
neil
346acc3f33 Update copilot-instructions.md 2026-04-05 10:40:35 +08:00
neil
a79bdb9ef3 Merge branch 'dev' of https://github.com/acmesh-official/acme.sh into dev 2026-04-04 22:24:13 +08:00
neil
563415d21f
Merge pull request #6895 from acmesh-official/dev
sync
2026-04-04 21:19:05 +08:00
neil
c2c5c3cdb7 Update copilot-instructions.md 2026-04-04 21:18:36 +08:00
neil
4aeb7bbab0 fix rule 2026-04-04 21:18:36 +08:00
orangepizza
12f639116c Update copilot instruction to match actual PR rule (#6873)
old version had instruction to use bash-only [[ ]] test, remove it and add rules for DNS script writing from https://github.com/acmesh-official/acme.sh/issues/343
2026-04-04 21:18:36 +08:00
Tomáš Pavlič
4cb1c6e1ea [dnsapi] add subreg.cz dns hook (#6848)
* Add DNS hook for subreg.cz
2026-04-04 21:18:36 +08:00
heximcz
9aad08ef14 Add BEST-HOSTING DNS API (#6859)
* Add BEST-HOSTING DNS API
2026-04-04 21:18:36 +08:00
neil
3b503a009c fix https://github.com/acmesh-official/acme.sh/issues/4924#issuecomment-4069887654 2026-04-04 21:18:36 +08:00
neil
13d6496653 fix https://github.com/acmesh-official/acme.sh/issues/6866#issuecomment-4080403721 2026-04-04 21:18:36 +08:00
neil
605299947e format 2026-04-04 21:18:36 +08:00
neil
d050f3458b don't switch from test back to production ca 2026-04-04 21:18:36 +08:00
CZECHIA-COM
d66264e741 Add dns_czechia DNS API plugin (#6764)
* Create dns_czechia.sh

This PR adds a DNS API plugin for CZECHIA.COM / RegZone (ZONER a.s.).
2026-04-04 21:18:36 +08:00
neil
cf9c70a6c7 Update copilot-instructions.md 2026-04-04 21:16:40 +08:00
Florian Heigl
ef49a9fd23
Update synology_dsm.sh (#6894)
quote variable name so message isn't missing the variable that needs to be fixed.
this was reported in #2727  (feedback for hook)
2026-04-03 11:38:21 +08:00
neil
fe5d2e3ef7 fix rule 2026-04-03 11:33:05 +08:00
orangepizza
bf486bb988
Update copilot instruction to match actual PR rule (#6873)
old version had instruction to use bash-only [[ ]] test, remove it and add rules for DNS script writing from https://github.com/acmesh-official/acme.sh/issues/343
2026-03-28 09:26:47 +08:00
Tomáš Pavlič
3c8c735362
[dnsapi] add subreg.cz dns hook (#6848)
* Add DNS hook for subreg.cz
2026-03-22 11:40:44 +08:00
heximcz
c397bd6573
Add BEST-HOSTING DNS API (#6859)
* Add BEST-HOSTING DNS API
2026-03-22 11:36:29 +08:00
neil
e26ce2f19c fix https://github.com/acmesh-official/acme.sh/issues/4924#issuecomment-4069887654 2026-03-21 13:02:16 +08:00
neil
af5e592fe4 fix https://github.com/acmesh-official/acme.sh/issues/6866#issuecomment-4080403721 2026-03-21 10:47:11 +08:00
neil
e21be4455f format 2026-03-21 10:41:58 +08:00
neil
5842e6ff4f don't switch from test back to production ca 2026-03-21 10:40:10 +08:00
CZECHIA-COM
8aea731bd4
Add dns_czechia DNS API plugin (#6764)
* Create dns_czechia.sh

This PR adds a DNS API plugin for CZECHIA.COM / RegZone (ZONER a.s.).
2026-03-16 20:17:18 +08:00
neil
5d158b1640
Merge pull request #6857 from acmesh-official/dev
sync
2026-03-13 20:19:12 +08:00
neil
3198c1af6e fix https://github.com/acmesh-official/acme.sh/issues/6856#issuecomment-4054175916 2026-03-13 20:10:22 +08:00
neil
fc8a61f10f update 2026-03-13 18:41:02 +08:00
neil
d08b4de794
Merge pull request #6832 from acmesh-official/dev
sync
2026-03-08 18:51:51 +08:00
neil
cc677ba9f1 minor 2026-03-08 18:43:39 +08:00
Ludwig
03860a4978
dnsapi/dns_me: ignore "already exists" error for multi-domain support (#6830) 2026-03-08 18:37:29 +08:00
neil
da5e818907
Merge pull request #6796 from Tra5is/add_technitium_expiry_ttl
Add Expiry TTL option for Technitium DNS API
2026-03-04 15:17:02 +08:00
Travis
f0146bd90e
shfmt edit 2026-03-03 05:31:51 -08:00
Tra5is
20722ea030
Merge branch 'dev' into add_technitium_expiry_ttl 2026-03-03 04:59:54 -08:00
neil
fdd2e4f19a add copilot 2026-03-01 13:13:07 +08:00
neil
89c86efc56
Merge pull request #6793 from ecm75/fix_kemplm
fixed checking for existing domain on loadmaster
2026-02-23 19:37:06 +08:00
Eike-Christian Müller
8ca1c83b95
Fixed missing error return value when certificate upload fails. 2026-02-23 09:24:06 +01:00
neil
8304d3e323
Merge pull request #6800 from pgerber/ipv6-debug
Fix IPv6 URL when trying to fetch challenge ourselves for debugging
2026-02-22 22:35:25 +08:00
Peter Gerber
b5bfd08e35
Fix IPv6 URL when trying to fetch challenge ourselves for debugging
Fixes the following error when debugging is enabled:

[Sat Feb 21 04:00:22 CET 2026] Here is the curl dump log:
[Sat Feb 21 04:00:22 CET 2026] * URL rejected: Port number was not a decimal number between 0 and 65535
* closing connection #-1

IPv6 addresses in URLs need to be written like this:

    http://[2001:43:5::250]
2026-02-21 04:03:41 +00:00
Travis
70f9e255d3
Add Expiry TTL option for Technitium DNS API 2026-02-18 09:29:47 -08:00
neil
12f147bf40
Merge pull request #6794 from infinitydev/pvefix
Add error check to Proxmox deploy hooks
2026-02-18 12:14:52 +08:00
infinitydev
bef0fdb1ae
check Proxmox Backup Server API response for errors 2026-02-17 19:28:25 +01:00
infinitydev
020a4bb5b3
check Proxmox VE API response for errors 2026-02-17 12:12:17 +00:00
emueller
acaaca89ab fixed checking for existing domain on loadmaster 2026-02-17 11:03:53 +01:00
neil
bdfa988b65
Merge pull request #6436 from invario/localcopy-deploy-hook
Add 'localcopy' deploy-hook
2026-02-14 11:01:40 +08:00
neil
0e93d2bee1
Merge pull request #6786 from alexandergott-afk/dev
Allow more than one DNS server for HA environments #6785
2026-02-13 11:13:50 +08:00
alexandergott-afk
83424e7ba4
Add the information from my last accepted pull that the TSIG key is optional. 2026-02-12 15:32:53 +01:00
alexandergott-afk
5bc44caf50
Allow more than one DNS server for HA environments 2026-02-12 15:27:23 +01:00
alexandergott-afk
47412d1822
Update dns_nsupdate.sh 2026-02-12 15:24:45 +01:00
neil
f68a758974
Merge pull request #6775 from alexandergott-afk/master
Add some fixes for dns_nsupdate provider
2026-02-09 20:16:49 +08:00
alexandergott-afk
fb4ae10e08
Merge branch 'dev' into master 2026-02-09 10:45:31 +01:00
neil
824e7b3d80
Merge pull request #6709 from invario/docker-non-root-supercronic
Docker with non-root using supercronic
2026-02-09 17:43:28 +08:00
alexandergott-afk
34052e56d7
too many spaces removed 2026-02-09 10:39:27 +01:00
alexandergott-afk
3374c22169
Merge branch 'dev' into master 2026-02-09 10:03:58 +01:00
alexandergott-afk
4807df0c3e
Fix Tab 2026-02-09 10:03:26 +01:00
neil
b79146281c
Merge pull request #6767 from dga-nagra/panos-api-key
Fix bug + Allow panos deploy-hook to use PANOS_KEY
2026-02-07 22:15:28 +08:00
neil
ab3093d58c
Merge pull request #6472 from UncleVic/master
feat: #6471 add message_thread_id to telegram notifications
2026-02-07 22:11:42 +08:00
neil
c21db5c5d5
Change shebang to use env for portability 2026-02-07 22:10:48 +08:00
alexandergott-afk
ec0dc40ad8
Merge branch 'dev' into master 2026-02-05 10:39:19 +01:00
alexandergott-afk
7236ba2d7c
Update dns_nsupdate.sh 2026-02-05 09:20:51 +01:00
alexandergott-afk
b440e49164
Update dns_nsupdate.sh 2026-02-05 09:14:18 +01:00
neil
80e39eb63c
Merge pull request #6687 from kapouer/ssh-noscp-permissions
Ssh noscp permissions
2026-02-04 18:49:52 +08:00
dga-nagra
61e986f23c
Conditionnaly change permissions (#1) 2026-02-04 11:01:44 +01:00
David Gallay
6a60695549 Allowing panos deploy-hook to only depend on PANOS_KEY.
Previous version add bugs that were not properly using the _api_key.

It also enforced to provide PANOS_USER and PANOS_PASSWORD which can be very constraining.

PANOS_KEY now has precedence. If not provided, the script falls back to PANOS_USER and PANOS_PASSWORD.
2026-02-03 10:51:42 +01:00
neil
28c088611a
Merge pull request #6440 from jf-lines/dns_infomaniak_API_v2
dns_infomaniak: upgraded to API v2. v1 is deprecated.
2026-02-01 17:15:02 +01:00
JF DAGUIN
274fd53c83
Merge branch 'dev' into dns_infomaniak_API_v2 2026-01-30 23:08:17 +01:00
JF DAGUIN
a4595f334e
Merge branch 'acmesh-official:master' into dns_infomaniak_API_v2 2026-01-30 23:04:36 +01:00
JF DAGUIN
477277bd2d
Rewrite token scope and URL to add one
Updated comments for clarity and formatting.
2026-01-30 23:00:25 +01:00
neil
d8053ed6b2
Merge pull request #6760 from OpusDNS/add-opusdns-provider
[CLOUD-31] Add acme.sh OpusDNS provider
2026-01-24 07:51:13 +01:00
neil
e44809c180 Update acme.sh 2026-01-24 07:49:18 +01:00
Kilian Ries
9c245eb37a fix: start zone detection from i=1 per acme.sh convention 2026-01-23 09:20:00 +01:00
Kilian Ries
713251c65b
Merge branch 'dev' into add-opusdns-provider 2026-01-22 18:34:04 +01:00
Kilian Ries
2e85e6f9bb Fix zone detection: check for dnssec_status instead of name
The error response also contains 'name' in 'zone_name' field,
causing false positives. Check for 'dnssec_status' which only
exists in valid zone responses.
2026-01-22 18:13:51 +01:00
Kilian Ries
9e584e346d Refactor: Add _opusdns_api helper, simplify code
- Added _opusdns_api() for all API requests
- Added _opusdns_init() for config initialization
- Removed duplicate code in add/rm functions
- Removed dead code (orphaned lines 173-178)
- Script now 158 lines (was 255, originally 379)
2026-01-22 18:13:09 +01:00
Kilian Ries
163eb1acb9 Simplify zone detection with API check
Instead of fetching all zones and matching, iterate through domain
parts and check each against the API until a valid zone is found.
Same approach as GoDaddy DNS plugin.

Example: _acme-challenge.test.example.com
- Try: test.example.com → 404
- Try: example.com → 200 ✓ → zone found!

Script reduced from 304 to 255 lines.
2026-01-22 18:11:01 +01:00
Kilian Ries
25a3ee48df Fix zone pagination parsing
- Fixed jq path: .pagination.has_next_page instead of .has_next_page
- Fixed grep fallback: remove rrsets before extracting zone names
  to avoid matching nested 'name' fields
- Simplified has_next_page detection with simple grep -q
- Added debug output for page zones and has_next status
2026-01-22 17:58:09 +01:00
Kilian Ries
30c9332327 Remove custom DNS propagation check
acme.sh handles DNS propagation checking internally via --dnssleep
and _check_dns_record. Custom propagation checks are unnecessary
and can conflict with acme.sh's own timing.

Removed:
- _opusdns_wait_for_propagation() function
- OPUSDNS_Polling_Interval option
- OPUSDNS_Propagation_Timeout option

Script is now consistent with other DNS API implementations
(Cloudflare, AWS, etc.) which don't have custom propagation checks.
2026-01-22 17:28:41 +01:00
Kilian Ries
dc65223da1 Remove all trailing whitespace for shfmt compliance 2026-01-22 17:10:12 +01:00
Kilian Ries
01c93b9bbd Fix shellcheck and shfmt issues
- Add double quotes around variables to prevent globbing
- Fix parameter expansion quoting in ${domain%.${_zone}}
- Remove trailing whitespace for shfmt compliance
2026-01-22 17:04:43 +01:00
Kilian Ries
b4ac517c7b Trigger DNS workflow for OpusDNS testing 2026-01-22 16:58:09 +01:00
Kilian Ries
bfd1f9bf6c [CLOUD-31] add acme.sh opusdns provider 2026-01-22 16:43:26 +01:00
neil
0cef5edac2 fix https://github.com/acmesh-official/acme.sh/issues/6196#issuecomment-3777530678 2026-01-21 20:00:27 +01:00
neil
8c384a92bf
Merge pull request #6755 from zbindenp/master
Fix dns_cyon to use correct regex
2026-01-18 14:30:53 +01:00
neil
ce2d9e9746
Merge pull request #6485 from Fangliding/master
Fix ipv6 cert cannot be found
2026-01-18 14:29:33 +01:00
Patrick Zbinden
282b048557 Fix dns_cyon to use correct regex 2026-01-14 21:05:32 +01:00
neil
f39d066ced
Merge pull request #6754 from acmesh-official/dev
sync
2026-01-13 21:50:14 +01:00
neil
e2882c536b disable notifications for myself
disable notifications for myself
2026-01-13 21:43:46 +01:00
neil
892b3ca219 fix account.conf permission
https://github.com/acmesh-official/acme.sh/issues/6708#issuecomment-3745737079
2026-01-13 21:28:14 +01:00
neil
4de00d4603
Merge pull request #6748 from acmesh-official/dev
sync
2026-01-11 21:50:00 +01:00
neil
40b29c1879 support Haiku OS 2026-01-11 21:37:02 +01:00
neil
d57ab0ab7d fix haiku 2026-01-11 21:28:45 +01:00
neil
d8c062defb fix haiku 2026-01-11 21:21:08 +01:00
neil
3e36b61823 fix haiku 2026-01-11 21:14:29 +01:00
neil
09009794e0 fix 2026-01-11 21:10:37 +01:00
neil
5e670e0d93 support haiku 2026-01-11 21:02:57 +01:00
neil
bc646d120e
Merge pull request #6746 from d-kurz/dev
dns_dynv6: Allow challenge strings that contain "id"
2026-01-10 12:54:47 +01:00
neil
c58da45917
Merge pull request #6678 from fratoro/master
Update dns_cyon to use unique user-agent and all cookies
2026-01-10 12:52:24 +01:00
Denis Kurz
b37867d027 remove unused code 2026-01-10 01:44:07 +01:00
Denis Kurz
880d93f7f7 fix(dynv6): allow 'id' in dns challenge
If the random dns challenge string happens to contain 'id',
the parsing method passed a broken, mingled mix of the
record's data and id field, instead of just the id. As a result,
deleting the TXT record failed. We now specifically look for
'"id":', which cannot appear as part of the challenge string.
2026-01-10 01:44:07 +01:00
Denis Kurz
5ad2bea129 fix typos 2026-01-10 01:44:07 +01:00
neil
9e51432a6f
Merge pull request #6745 from acmesh-official/dev
sync
2026-01-06 08:55:01 +01:00
neil
903a53991d fix bugs 2026-01-05 22:06:43 +01:00
neil
188aa515ce
Merge pull request #6724 from h-haaks/base64-encode-pfx-password
Base64 encode pfx password
2026-01-05 21:46:34 +01:00
neil
6a9776b1f0
Merge pull request #6631 from rperper/dev
QUIC.cloud DNS API support (dns_api.sh)
2026-01-05 21:42:13 +01:00
neil
2092d6061b fix https://github.com/acmesh-official/acme.sh/issues/6736#issuecomment-3707981300 2026-01-05 21:38:37 +01:00
neil
35f99c545c add debug wiki 2026-01-05 21:11:07 +01:00
neil
4b278dc1bb
Merge pull request #6735 from JensSpanier/dev
Pushover: Remove asterisks
2026-01-05 20:54:00 +01:00
neil
9bf5179b55
Merge pull request #6739 from moaminsharifi/master
Add Virakcloud dns api
2026-01-05 20:51:43 +01:00
Amin Sharifi
ca94221d08
Merge branch 'dev' into master 2026-01-04 18:26:04 +03:30
Amin Sharifi
70462b5ac3 run ~/shfmt -l -w -i 2 dnsapi/dns_virakcloud.sh and Remove unnecessary blank lines in dns_virakcloud.sh 2026-01-04 16:33:37 +03:30
Amin Sharifi
ef035248c3 Add VirakCloud DNS API support 2026-01-04 16:29:30 +03:30
Amin Sharifi
877cbe04c9 Add VirakCloud DNS API support with add and remove TXT record functions 2026-01-04 16:18:18 +03:30
Jens Spanier
94c670a759
Remove asterisks and line breaks 2026-01-04 11:49:35 +01:00
neil
045e4dee2e use openindiana-vm@v1 2026-01-02 16:24:06 +01:00
neil
06d9ca61a3
Merge pull request #6728 from jwaterwater/feat/qiniu-force-https-config
Feat/qiniu force https config
2026-01-02 15:36:09 +01:00
neil
dc6a996cc3
Merge pull request #6731 from jdevera/posix_globs_for_list
Fix `--list` regression in POSIX / Docker environments
2026-01-02 15:34:27 +01:00
Jacobo de Vera
b08bb2ef69 Fix list command for POSIX sh by avoiding brace expansion 2026-01-02 12:16:46 +00:00
风扇滑翔翼
64ac537e72
Merge branch 'dev' into master 2026-01-02 14:52:10 +08:00
风扇滑翔翼
ef2089ceb1
Update directory iteration pattern in acme.sh 2026-01-02 14:48:11 +08:00
Your Name
2ad984d8ad feat(qiniu): make forceHttps configurable via environment variable
Add QINIU_FORCE_HTTPS environment variable (default: false) to allow
configuring HTTPS redirect behavior for CDN domains.
2026-01-01 13:26:02 +00:00
Bob Perper
6a37f23b14 Ran shfmt locally 2025-12-30 16:31:16 -05:00
Bob Perper
cf2f9ef251 Missed additional quotes 2025-12-30 16:27:21 -05:00
Bob Perper
f9ffdbe407 Initialize record_id 2025-12-30 16:23:01 -05:00
Bob Perper
778b4a38ed Missed several double quote issues 2025-12-30 16:17:03 -05:00
Bob Perper
397c0605e5 Double quote for globbing 2025-12-30 16:11:39 -05:00
Bob Perper
b4f30ff026 Updated for shfmt 2025-12-30 16:07:01 -05:00
Bob Perper
0b66acf332 shfmt fixes 2025-12-30 15:54:23 -05:00
Bob Perper
185d92f1e7 shfmt fixes 2025-12-30 15:50:39 -05:00
Bob Perper
e031457cfa shfmt fixes 2025-12-30 15:48:29 -05:00
Bob Perper
162cfebbbb Removed jq requirement 2025-12-30 15:36:06 -05:00
Bob Perper
66ef351f36 Merge branch 'dev' of https://github.com/rperper/acme.sh into dev 2025-12-30 15:35:23 -05:00
invario
6a98b9f81e
chown /acme.sh to non-root user and set HOME to /acme.sh
Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2025-12-30 12:44:46 -05:00
invario
4219f7b2f6
align logic to acme.sh installcert(), fix perms on non-key files
Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2025-12-30 11:41:50 -05:00
jwaterwater
e260b86414
Merge branch 'acmesh-official:master' into master 2025-12-30 21:48:28 +08:00
Hugo Haakseth
21d52b5995
Store pfx password base64 encoded 2025-12-30 10:57:12 +01:00
invario
e03f8d3ad6
fix: savedeployconf for DEPLOY_LOCALCOPY_CERTIFICATE
Co-authored-by: Kevin Hoser <45083826+hoser21@users.noreply.github.com>
2025-12-28 12:03:02 -05:00
invario
6f5a0c5d5e
have entry.sh (instead of dockerfile) generate crontab file
Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2025-12-28 11:40:18 -05:00
invario
57db388932
Docker with non-root using supercronic
Replaces cronie with supercronic to allow non-root users to have
cronjobs. Creates user/group acme:acme UID:1000/GID:1000 with
home directory pointing to LE_CONFIG_HOME (default: /acme.sh)
'crontab' is generated in LE_CONFIG_HOME which is used by
supercronic.

Note that `acme.sh --installcronjob` and `--uninstallcronjob`
when run as a non-root user will fail but neither of should be
used in `daemon` mode anyway.

Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2025-12-28 11:39:59 -05:00
neil
d795cb4850
Merge pull request #6717 from acmesh-official/ari
Ari
2025-12-27 12:29:55 +01:00
neil
1bd2922bc3
Merge pull request #6716 from acmesh-official/dev
sync
2025-12-27 11:49:21 +01:00
neil
47f24126f5 Update supported OS table in README
add openindiana
2025-12-27 11:47:16 +01:00
neil
76fdac59bc Add OpenIndiana CI workflow and badge
Introduced a new OpenIndiana job to the DNS GitHub Actions workflow for testing, including necessary environment variables and steps. Updated README to display the OpenIndiana workflow status badge.
2025-12-27 11:41:58 +01:00
neil
49a3d586a3 Add OpenIndiana CI workflow
Introduces a GitHub Actions workflow for OpenIndiana to automate testing of shell scripts.
2025-12-27 11:37:39 +01:00
neil
361e7c5ad4 use nfs for vms
use nfs for vms
2025-12-27 11:26:59 +01:00
neil
3ae9711892
Merge pull request #6715 from acmesh-official/dev
sync
2025-12-27 11:22:22 +01:00
neil
fc7168e11d change default renew to 30 days
change default renew to 30 days
and fix readme
2025-12-27 11:20:22 +01:00
neil
260df0048b
Merge pull request #6241 from tomo2403/multideploy-yaml
Multideploy: Deploy to multiple hooks of the same type
2025-12-27 11:04:17 +01:00
invario
11cae37405 make compatible with both yq versions
kislyuk yq (used by Debian packages) does not accept `yq e` and
also returns strings with double quotes.

mikefarah's yq-go (used by Alpine) accepts `yq e` and `yq`.

replace `yq e` with `yq` and also use `-r` switch to remove double
quoting to ensure uniform return values from both yq versions.

Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2025-12-27 11:02:40 +01:00
tomo
61b59831c4 minor code style adjustments in multideploy script 2025-12-27 11:02:40 +01:00
Jacobo de Vera
96f38655b4 Use file descriptor 3 for main deployment loop
Before this, some deployment scripts would interact with STDIN and that would cause this loop to skip some elements. By using descriptor 3 we avoid clashing with the very common stdin and stdout.
2025-12-27 11:02:40 +01:00
Jacobo de Vera
ab7835ec58 Fix eval bug by quoting
Before this, the eval call would try to run some commands (if they were compound commands) in the yaml file on the machine running acme.sh

Eval might not be worth it for the little benefit it brings.
2025-12-27 11:02:40 +01:00
Jacobo de Vera
b8b1f1e9b4 Remove config logging when checking
Because it causes a mysterious crash and it's honestly not worth it.
2025-12-27 11:02:40 +01:00
Jacobo de Vera
6b66e734a9 Remove explicit save of the deployhook
acme.sh takes care of that
2025-12-27 11:02:40 +01:00
Jacobo de Vera
4f0a4850a6 Remove unnecessary resetting of IFS 2025-12-27 11:02:40 +01:00
Jacobo de Vera
8a78865174 Quote paths to prevent word splitting 2025-12-27 11:02:40 +01:00
Jacobo de Vera
69dd2cf78b Explain _clear_envs rationale 2025-12-27 11:02:40 +01:00
Jacobo de Vera
a961e03a59 Explain the use of eval 2025-12-27 11:02:40 +01:00
Jacobo de Vera
1eee4dee9c Update dependency name from yq to yq-go 2025-12-27 11:02:40 +01:00
Jacobo de Vera
1d8788767f Make failure to check file stop the deployment
Before this, checker issues were only logged. This stops the deployment if any configuration is incorrect.
2025-12-27 11:02:40 +01:00
Jacobo de Vera
986a6138eb Fix config file checks
The config file checks were returning okay even when there were errors. The yq tool returns "null" when it cannot find what's queried, but exists with a 0 rc still.
2025-12-27 11:02:40 +01:00
Jacobo de Vera
f850e8d0e4 Support spaces in service names
- Prefer using a pipe to `while read`
- But use a temp file when the loop needs to modify variables that need to be used outside the loop, as the pipe creates a subshell and modifications do not survive after the loop exits.
2025-12-27 11:02:40 +01:00
tomo
d375012c5d fixed yml file env list 2025-12-27 11:02:40 +01:00
tomo
37c25aa107 removed unneeded return value 2025-12-27 11:02:40 +01:00
tomo
7b16526e7f removed dead code 2025-12-27 11:02:40 +01:00
tomo
093f36b4d6 implemented exiting with the number of failed deployments 2025-12-27 11:02:40 +01:00
tomo
e5b47f6402 implemented exiting with 1 if at least one deployment fails 2025-12-27 11:02:40 +01:00
tomo
a55d40be97 fixed bug with envs due to the use of a wrong function 2025-12-27 11:02:40 +01:00
tomo
c1c49d5a01 simplified deploy method 2025-12-27 11:02:40 +01:00
tomo
88e4d64c1a fixed IFS problems for some hooks 2025-12-27 11:02:40 +01:00
tomo
17e0bbcbb6 fixed formatting 2025-12-27 11:02:40 +01:00
tomo
95c7546051 removed configs and implemented specification of deploy file name 2025-12-27 11:02:40 +01:00
tomo
c1e17c366f Update links in multideploy.sh 2025-12-27 11:02:40 +01:00
tomo
88cde7be6d fixed missing wiki link 2025-12-27 11:02:40 +01:00
tomo
c16e059535 allowed using varaibles in deploy file 2025-12-27 11:02:40 +01:00
tomo
88d4637ee3 added header doc 2025-12-27 11:02:40 +01:00
tomo
74ed0354a3 added docs and enhanced log messages 2025-12-27 11:02:40 +01:00
tomo
2cc5e66517 added docs 2025-12-27 11:02:40 +01:00
tomo
ba7c368ee5 fixed IFS problems 2025-12-27 11:02:40 +01:00
tomo
768de270bf improved preprocessing and fixed bug with wrong param of services 2025-12-27 11:02:40 +01:00
tomo
db1dc4de0d added debug messages 2025-12-27 11:02:40 +01:00
tomo
fb0926dc81 implemented checking for different kinds of deploy file 2025-12-27 11:02:40 +01:00
tomo
34eb2a655a added yq to dockerfile 2025-12-27 11:02:40 +01:00
tomo
23e1a53ec8 implemented deploying to services 2025-12-27 11:02:40 +01:00
tomo
67d58a12e7 implemented handling envs 2025-12-27 11:02:40 +01:00
tomo
0ed5e21232 fixed formatting and private var names 2025-12-27 11:02:40 +01:00
tomo
b2eb1d2bbc refactored getting services 2025-12-27 11:02:40 +01:00
tomo
3c184486c3 fixed indents 2025-12-27 11:02:40 +01:00
tomo
5a730bf00d implemented checking deploy file 2025-12-27 11:02:40 +01:00
neil
cbd5dae3b4
Merge pull request #6714 from xiagw/fix-ali-cdn-timestamp
fix: update timestamp variable for ali CDN and DCDN SSL certificate queries
2025-12-27 11:00:06 +01:00
xiagw
b9c877adb9
fix: update timestamp variable for CDN and DCDN SSL certificate queries 2025-12-27 14:33:03 +08:00
neil
d24c7e977e
Merge pull request #6697 from sergiustheblack/fix/strongswan
Fix strongswan deploy hook
2025-12-25 13:39:05 +01:00
neil
6a1ff1c0a6
Merge pull request #6710 from erfantkerfan/dev
improve: change sotoon api to v2.1 with simplification
2025-12-25 13:36:06 +01:00
jwaterwater
10126410b6
Merge branch 'dev' into master 2025-12-25 15:09:08 +08:00
jwaterwater
f4a575fee1 bug fixed 2025-12-25 14:48:44 +08:00
Erfan Gholizade
383557df61 improve: change sotoon api to v2.1 with simplification 2025-12-24 17:19:17 +03:30
neil
dba4be8065
Merge pull request #6696 from taoso/dev
Support list IPv6 address certificate
2025-12-23 21:42:01 +01:00
Bob Perper
f1aac43f0f Retry for workflow 2025-12-23 09:10:49 -05:00
Bob Perper
94783f46ad Retry to pass workflow 2025-12-23 07:53:33 -05:00
Bob Perper
1d26d4fc91 Detect missing jq 2025-12-22 16:42:26 -05:00
Bob Perper
7fc45226da
Merge branch 'acmesh-official:dev' into dev 2025-12-22 16:38:10 -05:00
neil
a670c07caf
Merge pull request #6692 from as-kholin/omglol_202512
Minor updates to the Omg.lol DNS API
2025-12-22 20:09:23 +01:00
neil
09cc2bdfa5
Merge pull request #6690 from flybyray/issue6688
inwx API change
2025-12-22 20:06:16 +01:00
neil
cc897cab4c
Merge pull request #6655 from hostup/dev
Add: Hostup DNS provider
2025-12-22 19:48:07 +01:00
neil
7ba9597928
Merge pull request #6659 from erfantkerfan/master
Added Sotoon dns api
2025-12-22 19:42:12 +01:00
neil
d2d862420e
Merge pull request #6705 from mq00fc/dev
Fix ali-cdn deploy hook
2025-12-22 19:41:00 +01:00
DreamSlave
fd6a14de8a
Update timestamp function in ali_dcdn.sh 2025-12-22 17:00:31 +08:00
DreamSlave
0eb40c6ce6
Update timestamp variable in ali_cdn.sh 2025-12-22 17:00:22 +08:00
hostup
e321b3c75c
Update dns_hostup.sh 2025-12-22 04:53:21 +01:00
hostup
03d8d3bc1b
Update dns_hostup.sh 2025-12-22 04:29:50 +01:00
Erfan Gholizade
f85de2b0d3 Added Sotoon dns api
handle the case of metadata 404 in old api domains

fix index of domain start

fix shfmt

Revert "handle the case of metadata 404 in old api domains"

This reverts commit 9fe4616664b897c9891271006e7489b10bb818ca.

fix 404 on dot ad hyphen

fix shfmt
2025-12-21 11:50:13 +03:30
Robert Rettig
5fb42b7339
remove prior additions which tried to use oathtool in tests 2025-12-20 14:53:01 +01:00
Robert Rettig
1b2630dc0d
stop using oathtool 2025-12-20 13:26:38 +01:00
海涛
3fb4c313ec Support list IPv6 address certificate 2025-12-20 10:05:52 +08:00
Robert Rettig
65892453be
take a fork of acmetest 2025-12-19 19:23:13 +01:00
Robert Rettig
cba0ff8321
Add oathtool to ubuntu package list in plat.conf 2025-12-19 19:09:23 +01:00
Robert Rettig
b6523c2301
Install oathtool in container via _setup function 2025-12-19 18:42:28 +01:00
Robert Rettig
e92d0a7492
Fix: Install oathtool at runtime in test container 2025-12-19 18:20:56 +01:00
Robert Rettig
0e5aab346f
Add oathtool to acmetest Docker image for 2FA support 2025-12-19 18:17:05 +01:00
Robert Rettig
a5ad15be02
Add oathtool to Docker job for 2FA support 2025-12-19 18:07:46 +01:00
Robert Rettig
27ebf09c5c
Improve _htmlEscape function robustness by using printf instead of echo
small commit to trigger github actions
2025-12-19 18:02:18 +01:00
Robert Rettig
1c65c04b54
update dns_inwx_info 2025-12-19 17:51:13 +01:00
Robert Rettig
987882ea37
fix delete 2025-12-19 17:49:24 +01:00
Sergey Parfenov
00aaed1b14 Fix strongswan deploy hook
Make it more resistant to deploy hooks api change by passing custom arguments first
2025-12-19 18:12:45 +03:00
Robert Rettig
e3b1bccb6a Fixes to support INWX again
Fixes #6688
2025-12-18 16:00:55 +00:00
Jérémy Lal
6ca19fb003
Ensure ssh.sh sets 600 permissions on keyfile 2025-12-18 09:55:50 +01:00
as-kholin
85ff92170b
Updated comment to be more clear on variable vs.
definition
2025-12-17 17:15:10 -05:00
Gary McGregor
7ac8c6c75b Merge remote-tracking branch 'upstream/dev' into omglol_202512 2025-12-17 16:56:26 -05:00
neil
6004e7f5cd
Merge pull request #6675 from imiric/fix/exoscale-dns
fix: update Exoscale DNS script
2025-12-17 20:49:41 +01:00
as-kholin
4a7e5d0720
Updating Auth header to satisfy shellcheck 2025-12-16 15:00:05 -05:00
as-kholin
b4042d5ccb
Updated checks for empty parameters to actually trigger, and added a validation check against the omg.lol API to confirm address and apikey are good before proceeding 2025-12-16 14:51:42 -05:00
Bob Perper
6f66e294de Yet another try 2025-12-11 15:43:15 -05:00
Bob Perper
5017c12324 Trying verification again 2025-12-11 09:03:38 -05:00
fratoro
70bc5a6fba
Update dns_cyon to use unique user-agent and all cookies 2025-12-09 22:57:26 +01:00
fratoro
3063973744
Update dns_cyon to use unique user-agent and all cookies 2025-12-09 16:49:12 +01:00
Bob Perper
1a2071a120
Merge branch 'acmesh-official:dev' into dev 2025-12-09 10:05:22 -05:00
Bob Perper
e5dea48d3c Retry pull request with HTTPS_INSECURE=1 2025-12-09 07:40:00 -05:00
Ivan Mirić
1413aa332b fix: update Exoscale DNS script
This updates the Exoscale DNS script to work with v2 of their API.
2025-12-09 09:49:54 +01:00
neil
37cf431e80
Merge pull request #6676 from acmesh-official/dev
sync
2025-12-08 21:17:08 +01:00
neil
0c9d2dafe3
Merge pull request #6638 from pini-gh/fix-6081
dnsapi/dns_gandi_livedns.sh: save Gandi LiveDNS variables with prefix 'SAVED_'
2025-12-08 21:14:48 +01:00
neil
e8708a7489 fix solaris 2025-12-08 21:13:00 +01:00
neil
79592c700f
Merge pull request #6671 from ufozone/dev
Add mgw-media.de DNS API
2025-12-08 20:14:00 +01:00
Markus G.
ad3783170e
Fix formatting issues in dns_mgwm.sh script 2025-12-08 19:31:40 +01:00
Gilles Filippini
329dab9a67 Use '_mutable' functions for authentication variables
Fixes #6081.
2025-12-08 19:02:33 +01:00
Markus G.
f142f37064 Remove DNS provider information comment
Removed comment about DNS provider information.
2025-12-08 18:49:51 +01:00
Markus G.
0d2955b48d Update documentation links in dns_mgwm.sh 2025-12-08 18:49:51 +01:00
Markus G.
95da407de8 Refactor DNS API script to use new request function 2025-12-08 18:49:51 +01:00
Markus G.
503ca1e9c2 Change MGWM_API_BASE to use IP address 2025-12-08 18:49:51 +01:00
Markus G.
d8722c46d9 Consolidate API request logic in dns_mgwm.sh
Refactor DNS API functions to use a unified request handler.
2025-12-08 18:49:51 +01:00
Markus G.
546c2d47d5 Refactor DNS API for mgw-media.de
Updated DNS API script for mgw-media.de to use new base URL and improved API request structure.
2025-12-08 18:49:51 +01:00
Markus G.
2ba615555c Refactor dns_mgwm.sh for improved API interaction
Refactor MGWM API script to improve clarity and functionality. Update API endpoint and streamline credential handling.
2025-12-08 18:49:51 +01:00
Markus G.
e94c6be4a1 Update MGWM API endpoint to IPv4 2025-12-08 18:49:51 +01:00
Markus G.
11eaad1fa7 Update API URLs to include .php extension 2025-12-08 18:49:51 +01:00
Markus G.
daf7f7c268 Refactor dns_mgwm.sh for better API integration
Refactor DNS API script to improve credential handling and update API endpoint.
2025-12-08 18:49:51 +01:00
ufozone
4965c704d7 Initial commit for mgw-media.de 2025-12-08 18:49:51 +01:00
neil
b7fe7a40ba
Merge pull request #6333 from EfficientIP-Labs/dns_efficientip
Implement DNS API for Efficientip SOLIDserver
2025-12-08 18:46:59 +01:00
Bob Perper
5fcca7c7e0 Retry correct commit 2025-12-08 08:48:30 -05:00
Bob Perper
b0088c82dc
Merge branch 'acmesh-official:dev' into dev 2025-12-08 08:44:42 -05:00
Bob Perper
875cf056b7 Submit dns_qc.sh for review 2025-12-08 08:40:44 -05:00
neil
3b2c2b16b2 minor 2025-12-06 11:23:28 +01:00
neil
45cb36f6d9 fix https://github.com/acmesh-official/acme.sh/issues/6246#issuecomment-3610998032 2025-12-05 22:31:45 +01:00
Bob Perper
ed1bd01592 Save account information differently 2025-12-05 16:29:49 -05:00
Bob Perper
a1857af6de Update error message and secrets 2025-12-05 15:53:17 -05:00
neil
70e965fd55
Merge pull request #6665 from acmesh-official/dev
sync
2025-12-05 21:51:56 +01:00
neil
bee01c938a add comment 2025-12-05 21:46:15 +01:00
Bob Perper
6b6d22c5ba shfmt updates 2025-12-05 13:52:59 -05:00
Bob Perper
67a389cbbf Minor change and setup secrets again 2025-12-05 10:35:52 -05:00
Bob Perper
d3930639db Updated secrets and put back guards 2025-12-05 08:58:41 -05:00
Bob Perper
65292b010e
Merge branch 'acmesh-official:dev' into dev 2025-12-05 08:57:01 -05:00
invario
f39a6fe517
Use cat instead of cp for all files
Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2025-12-04 11:06:50 -05:00
invario
2775def93a
Use 'cat' instead of 'cp', removed use of temp file, keeps permissions
Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2025-12-03 12:05:19 -05:00
neil
5cbae50ec1
Merge pull request #6651 from stefanriegel/master
Added dns provider for Infoblox Universal DDI
2025-12-02 20:00:09 +01:00
hostup
51b4fa0080
Update dns_hostup.sh 2025-12-01 17:19:16 +01:00
hostup
64a6ea68fa
Update dns_hostup.sh 2025-12-01 15:58:36 +01:00
hostup
d97b4477b2
Update dns_hostup.sh 2025-12-01 15:55:17 +01:00
hostup
b8e394e76a
Update dns_hostup.sh
bug fix Omnios fial
2025-12-01 15:48:48 +01:00
hostup
671d542898 chore: trigger DNS API test 2025-12-01 13:49:12 +01:00
hostup
9980ad0fef add HostUp DNS 2025-12-01 13:33:19 +01:00
Stefan
004deaeea1
Merge branch 'dev' into master 2025-11-30 19:28:39 +01:00
Stefan Riegel
36b8ca2bc0 Fix shfmt formatting: Remove trailing whitespace 2025-11-30 00:49:36 +01:00
Stefan Riegel
890ab4a7bb Refactor dns_infoblox_uddi.sh: Fix zone detection and add wildcard cert support
- Added _get_root() helper function for proper zone detection
- Fixed zone ID extraction to match dns/auth_zone/* pattern
- Added _infoblox_rest() wrapper for API calls with proper auth
- Improved error handling for authentication failures
- Added support for wildcard certificates (multiple TXT records)
- Filter by exact txtvalue when deleting records
- Follow acme.sh best practices and conventions

Tested with:
- Standard domain certificates
- Wildcard certificates (*.domain.com)
- Multiple subdomains
- Staging and production Let's Encrypt
2025-11-30 00:48:15 +01:00
Stefan Riegel
490b9e2d09 Clean up debug statements 2025-11-29 23:39:43 +01:00
Stefan Riegel
ca35e8c118 Fix zone_id extraction to query correct zone 2025-11-29 23:32:28 +01:00
Stefan Riegel
eeb91de6a3 Replace jq with shell-based JSON parsing 2025-11-29 23:13:52 +01:00
Stefan Riegel
657b7195d6 Fix Authorization header format 2025-11-29 23:06:26 +01:00
Stefan
054a73f297
fix DNS API 2025-11-29 22:48:06 +01:00
Stefan
5c6d8aacbe
Add files via upload 2025-11-29 22:38:02 +01:00
neil
ac0df6bc88 start 3.1.3 2025-11-29 16:36:14 +01:00
neil
f007b46c1b
Merge pull request #6644 from SunMar/patch-1
fix "dns_aws.sh: line 164: _error: command not found" #6443
2025-11-28 20:15:18 +01:00
SunMar
c5566eafeb
fix "dns_aws.sh: line 164: _error: command not found" #6443 2025-11-28 09:44:50 +01:00
asavin
75ee17aeeb Remove unecessary base64 encoding 2025-11-25 14:47:40 +01:00
Bob Perper
88e9681481 Additional shfmt issues 2025-11-21 16:26:24 -05:00
Bob Perper
20ef8cd369 Additional shfmt issues 2025-11-21 16:22:42 -05:00
Bob Perper
ded539b11c Additional shfmt issues 2025-11-21 16:04:23 -05:00
Bob Perper
5e76ea820c Additional shfmt issues 2025-11-21 16:01:14 -05:00
Bob Perper
90d2ff8fad Better fixes for shfmt errors 2025-11-21 15:27:38 -05:00
Bob Perper
0f42b06b48 Trying again to fix shfmt error 2025-11-21 08:02:59 -05:00
Bob Perper
b500ac3dbb Updated secret and dns_qc.sh 2025-11-20 15:38:56 -05:00
Bob Perper
894dfdd5d8 Merge branch 'QUIC.cloud' of github.com:rperper/acme.sh into QUIC.cloud 2025-11-20 08:02:11 -05:00
Bob Perper
d0d97a40a6 Commit to force initial test 2025-11-20 08:01:36 -05:00
Bob Perper
cf5fd403e8 Removed false wiki page 2025-11-20 08:01:36 -05:00
Bob Perper
72a6a5ce04 Added wiki doc 2025-11-20 08:01:36 -05:00
Bob Perper
9381835a7c QUIC.cloud support for acme.sh 2025-11-20 08:01:36 -05:00
Bob Perper
2c9ba9b3df Merge remote-tracking branch 'upstream/dev' into QUIC.cloud 2025-11-20 07:58:23 -05:00
Bob Perper
9a74c86327 Commit to force initial test 2025-11-19 09:06:55 -05:00
Bob Perper
692a21ee0d
Merge branch 'acmesh-official:master' into QUIC.cloud 2025-11-18 11:25:49 -05:00
EfficientIP-Labs
ad2cb507a4
Merge branch 'dev' into dns_efficientip 2025-11-12 14:11:54 +01:00
EfficientIP-Labs
dca23a98f1
Merge branch 'acmesh-official:master' into dns_efficientip 2025-11-12 10:50:42 +01:00
Bob Perper
68eb6defd3 Removed false wiki page 2025-10-27 12:04:08 -04:00
Bob Perper
e25e30dcdd Added wiki doc 2025-10-27 12:00:01 -04:00
Bob Perper
7ca8a9e449 QUIC.cloud support for acme.sh 2025-10-24 11:34:25 -04:00
Alexis Savin
8cb9713493
Merge branch 'acmesh-official:master' into dns_efficientip 2025-10-18 18:25:40 +02:00
asavin
ef76831d37 Addressing #pullrequestreview-3353279982 2025-10-18 14:16:51 +02:00
asavin
c4671272c0 Yet another push to try the test suite 2025-10-14 13:35:15 +02:00
EfficientIP-Labs
86f2584162
Merge branch 'acmesh-official:master' into dns_efficientip 2025-09-16 11:15:51 +02:00
EfficientIP-Labs
020c52e583
Merge branch 'acmesh-official:master' into dns_efficientip 2025-09-08 09:23:17 +02:00
风扇滑翔翼
8713918bdb Fix ipv6 cert cannot be found 2025-08-20 13:00:56 +08:00
Viktor Polyakov
45c4a98f1d feat: add message_thread_id to telegram notifications 2025-08-11 16:05:09 +03:00
invario
3252e0ce2e
Add outputs for PFX and PEM
Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2025-07-28 12:14:12 -04:00
Alexis Savin
eda8614754
Merge branch 'acmesh-official:master' into dns_efficientip 2025-07-18 12:13:37 +02:00
JF DAGUIN
f1209ce06a
Merge branch 'dev' into dns_infomaniak_API_v2 2025-07-16 20:39:47 +02:00
JF DAGUIN
5808b8d176
Merge branch 'dev' into dns_infomaniak_API_v2 2025-07-09 13:43:23 +02:00
pileus-lines
c2f8b4d1f2 Update dns_infomaniak.sh
because infomaniak API v1 no longer works
2025-07-09 12:13:17 +02:00
invario
014a781426
Create localcopy deploy-hook
Deploy-hook to very simply copy files to set directories and then execute whatever reloadcmd
the admin needs afterwards. This can be useful for configurations where the "multideploy"
hook (in development) is used or when an admin wants ACME.SH to renew certs but needs to
manually configure deployment via an external script (e.g. The deploy-freenas script for TrueNAS Core/Scale

https://github.com/danb35/deploy-freenas/

Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2025-07-06 20:12:10 -04:00
asavin
ca08ce4262 Fixing forgottent openssl ref 2025-06-23 08:59:33 +02:00
asavin
eb22a84db4 Merge branch 'dns_efficientip' 2025-06-11 16:47:36 +02:00
Alexis Savin
d66dd99621
Merge branch 'dev' into dns_efficientip 2025-06-11 16:33:09 +02:00
asavin
5a085f2514 Addressing #discussion_r2105799190 2025-05-25 18:37:17 +02:00
Alexis Savin
e79ee7fb74
Merge branch 'acmesh-official:master' into dns_efficientip 2025-05-25 18:27:39 +02:00
EfficientIP-Labs
718ff3a5f5
Merge branch 'acmesh-official:master' into dns_efficientip 2025-05-09 09:03:45 +02:00
asavin
a2e52dadb9 Triggering pipeline with DNS_WILDCARD 2025-04-25 09:59:40 +02:00
asavin
ca4cb018d0 Triggering another action pipeline 2025-04-25 09:59:40 +02:00
asavin
c2762d3b6f Triggering another action pipeline 2025-04-25 09:59:40 +02:00
asavin
947e872850 Triggering another action pipeline 2025-04-25 09:59:40 +02:00
asavin
3baa5e145f Triggering another action pipeline 2025-04-25 09:59:40 +02:00
asavin
67fd35127c Triggering another action pipeline 2025-04-25 09:59:40 +02:00
asavin
c421e2ddfc Triggering another action pipeline 2025-04-25 09:59:40 +02:00
asavin
42febe97b5 Triggering another action pipeline 2025-04-25 09:59:40 +02:00
asavin
74ca0fb763 Triggering another action pipeline 2025-04-25 09:59:40 +02:00
asavin
13631ea2de Triggering another action pipeline 2025-04-25 09:59:40 +02:00
asavin
a1eee5923a Disabling SC2034 2025-04-25 09:59:40 +02:00
asavin
af92bbca2a Disabling SC2034 2025-04-25 09:59:40 +02:00
asavin
c9287071e3 Fixing shellcheck issue 2025-04-25 09:59:40 +02:00
asavin
292026288a Fixing sh syntax 2025-04-25 09:59:40 +02:00
asavin
1f056998f3 Update for testing github action pipeline 2025-04-25 09:59:40 +02:00
asavin
4d7cb7de5f Update for testing github action pipeline 2025-04-25 09:59:40 +02:00
asavin
67855f21d4 Updating issue ID 2025-04-25 09:59:40 +02:00
asavin
8484565e95 Updating Options to meet OptionsAlt pre-requisites 2025-04-25 09:59:40 +02:00
asavin
f7d8abe8ea Fixing shellcheck issues 2025-04-25 09:59:40 +02:00
asavin
8ca90297e7 Remove export ? 2025-04-25 09:59:40 +02:00
asavin
e08f9080c2 Initial commit 2025-04-25 09:59:40 +02:00
asavin
419738fbd5 Triggering pipeline with DNS_WILDCARD 2025-04-24 16:05:20 +02:00
asavin
7f1423dd6f Triggering another action pipeline 2025-04-24 13:33:28 +02:00
asavin
9f09dcd18c Triggering another action pipeline 2025-04-24 11:13:38 +02:00
asavin
91081ade3c Triggering another action pipeline 2025-04-24 10:50:57 +02:00
asavin
4d933c23a8 Triggering another action pipeline 2025-04-24 10:43:46 +02:00
asavin
f29bfd995d Triggering another action pipeline 2025-04-24 10:15:56 +02:00
asavin
30d5d1aea9 Triggering another action pipeline 2025-04-22 18:01:29 +02:00
asavin
7a0450a7f4 Triggering another action pipeline 2025-04-22 17:55:23 +02:00
asavin
5bb09f469f Triggering another action pipeline 2025-04-22 17:06:33 +02:00
asavin
90e9d8ff52 Triggering another action pipeline 2025-04-22 16:38:37 +02:00
asavin
59a43ce5d1 Disabling SC2034 2025-04-22 16:27:36 +02:00
asavin
5bc01aa251 Disabling SC2034 2025-04-22 15:56:49 +02:00
asavin
9eeb979c7b Fixing shellcheck issue 2025-04-22 15:49:41 +02:00
asavin
eabd7592fe Fixing sh syntax 2025-04-22 15:41:22 +02:00
asavin
e089a3d8a1 Update for testing github action pipeline 2025-04-22 15:31:17 +02:00
asavin
7560375502 Update for testing github action pipeline 2025-04-22 15:23:11 +02:00
asavin
1f77b89266 Updating issue ID 2025-04-22 11:40:01 +02:00
asavin
7c610124d9 Updating Options to meet OptionsAlt pre-requisites 2025-04-22 11:32:23 +02:00
asavin
a0c5ef4e6f Fixing shellcheck issues 2025-04-22 11:17:14 +02:00
asavin
218934e767 Remove export ? 2025-04-18 18:06:12 +02:00
asavin
c6a9825c0a Initial commit 2025-04-18 17:25:55 +02:00
neil
ee661e5d71 fix format 2024-12-10 21:02:54 +01:00
neil
5ddffc9172 fix format 2024-12-10 21:01:37 +01:00
neil
40dd085ef8
Merge pull request #6159 from acmesh-official/dev
fix format
2024-12-10 20:59:27 +01:00
neil
d5b5bcef56 support ARI, not finished yet
https://github.com/acmesh-official/acme.sh/issues/4944
2024-12-10 20:54:20 +01:00
160 changed files with 16454 additions and 1641 deletions

190
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,190 @@
# GitHub Copilot Shell Scripting (sh) Review Instructions for acme.sh
## Overall Goal
Your role is to act as a rigorous yet helpful senior engineer, reviewing Shell script code (`.sh` files) for the [acme.sh](https://github.com/acmesh-official/acme.sh) project. Ensure the code exhibits the highest levels of robustness, security, and portability.
The review must focus on risks unique to Shell scripting, such as proper quoting, robust error handling, and the secure execution of external commands.
## Required Output Format
Organize the feedback into a single, structured report, using the three-level marking system:
1. **Critical Issues (Must Fix Before Merge)**
2. **Suggestions (Improvements to Consider)**
3. **Good Practices (Points to Commend)**
---
## Shell Compatibility
- **POSIX sh only** -- all scripts must target `sh`, not `bash`. No bash-isms allowed.
- **Shebang**: always use `#!/usr/bin/env sh` (not `#!/bin/sh`, not `#!/usr/bin/env bash`).
- **Use `return`, never `exit`** -- scripts are sourced, not executed as subprocesses. `exit` would kill the parent shell.
- **Cross-platform**: code must work on Linux, macOS, FreeBSD, Solaris, and BusyBox environments.
---
## Robustness and Error Handling
- **(Critical)** Enforce the use of the following combination at the start of the script for safety and robustness:
- `set -e`: Exit immediately if a command exits with a non-zero status.
- `set -u`: Treat unset variables as an error and exit.
- `set -o pipefail`: Ensure the whole pipeline fails if any command in the pipe fails.
- **Always check return values** of function calls. If an error occurs, there must be a way to stop execution.
- **Return 1** after `_err` messages:
```sh
if [ -z "$VARIABLE" ]; then
_err "VARIABLE is required"
return 1
fi
```
- Check for the use of `mktemp` when creating temporary files to prevent race conditions and security risks.
---
## Security and Quoting
- **(Critical)** Check that all variable expansions (like `$VAR` and `$(COMMAND)`) are properly enclosed in **double quotes** (i.e., `"$VAR"` and `"$(COMMAND)"`) to prevent **Word Splitting** and **Globbing**.
- **(Critical)** Find and flag any hardcoded passwords, keys, tokens, or authentication details.
- Verify that all user input, command-line arguments (`$1`, `$2`, etc.), or environment variables are rigorously validated and sanitized before use.
- Avoid `eval` -- warn against and suggest alternatives, as it can lead to arbitrary code execution.
---
## Use Built-in Helper Functions
Never use raw shell commands when acme.sh provides a wrapper function. This is the most critical rule for portability.
| Instead of | Use |
|---|---|
| `tr '[:upper:]' '[:lower:]'` | `_lower_case()` |
| `tr '[:lower:]' '[:upper:]'` | `_upper_case()` |
| `head -n 1` | `_head_n 1` |
| `openssl dgst` / `openssl` | `_digest()` / `_hmac()` |
| `date` | `_utc_date()` with `sed`/`tr` |
| `curl` / `wget` | `_get()` or `_post()` |
| `sleep` | `_sleep` |
| `base64` / `openssl base64` | `_base64()` |
| `$(( ))` arithmetic | `_math()` |
| `grep -E` / `grep -Po` | `_egrep_o()` |
| `printf` | `echo` |
| `idn` command | `_idn()` / `_is_idn()` |
| `mktemp` | `_mktemp()` |
| `[:space:]` | ` ` |
| `[:alnum:]` | `A-Za-z0-9` |
| `[:alpha:]` | `A-Za-z` |
| `[:digit:]` | `0-9` |
| `awk` | `cut` / `sed` / `while read` loops |
When fixing a pattern issue, fix **all instances** in the file, not just the one highlighted.
---
## Forbidden External Tools
Do not use these commands -- they are not portable across all target platforms:
- `jq` (parse JSON with built-in string manipulation)
- `grep -A` (removed throughout the project)
- `grep -Po` (Perl regex not available everywhere)
- `rev`, `xargs`, `iconv`
- If you must depend on an external tool, check with `_exists` first:
```sh
if ! _exists jq; then
_err "jq is required"
return 1
fi
```
- Warn against patterns like `for i in $(cat file)` or `for i in $(ls)` and recommend the more robust `while IFS= read -r line` pattern for safely processing file contents or filenames that might contain spaces.
---
## Configuration Management
Use the correct save/read functions depending on hook type:
- **DNS hooks**: `_readaccountconf_mutable` to read API keys, `_saveaccountconf_mutable` to save them. Do not use `_saveaccountconf` or `_readaccountconf`.
- **Deploy hooks**: `_savedeployconf` / `_getdeployconf`
- **Notification hooks**: use account conf functions.
- Save operations should only happen in the correct lifecycle function (e.g., `_issue()`).
- Use environment variables for all configurable values -- do not introduce hardcoded config files.
- Do not clear account conf without a clear reason.
---
## DNS API Conventions
- Read the [DNS API Dev Guide](https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide) before writing a DNS plugin.
- Each file under `dnsapi/` must contain a `{filename}_add` function for adding DNS TXT records.
- The `_get_root()` loop counter `i` must start from `1` (not `2`) to support DNS alias mode.
- The `dns_*_rm()` function must remove records **by TXT value**, not by replacing/updating. See [#1261](https://github.com/acmesh-official/acme.sh/issues/1261).
- Preserve the `dns_*_info` metadata variable block in each DNS script header.
---
## Variable Naming
- Use CamelCase with provider prefix: `KINGHOST_Username` (not `KINGHOST_username`).
- Variable names should use uppercase letters and underscores (e.g., `MY_VARIABLE`), or follow established project conventions.
- Avoid confusingly similar names. Prefer one variable with comma-separated values over multiple variables (e.g., `CZ_Zones` with comma support instead of separate `CZ_Zone` and `CZ_Zones`).
- Do not define variables with the same name in different scopes.
- Variables inside functions should be declared using the `local` keyword to avoid unintentionally modifying global state.
---
## Code Style
- Use `shfmt` for formatting -- CI enforces it.
- Reduce indentation where possible.
- Single space, not double spaces.
- No trailing semicolons after `return` statements.
- Add a newline at the end of every file.
- Use `$(command)` over backticks `` `command` `` for command substitution.
---
## Simplicity
- Prefer hardcoded sensible defaults over unnecessary configuration variables (e.g., use `3600` for TTL instead of a `DESEC_TTL` variable).
- Reject over-engineered solutions. If it can be done in one line, do it in one line.
- Follow existing patterns in the codebase -- new hooks should look like existing hooks.
- Respect user choices: do not `chmod` files that already exist; the user's permissions take priority.
---
## Documentation Requirements
Before a PR can be merged, the following documentation must be provided:
- **Wiki page**: add or update the relevant page:
- DNS APIs: [dnsapi](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) or [dnsapi2](https://github.com/acmesh-official/acme.sh/wiki/dnsapi2)
- Deploy hooks: [deployhooks](https://github.com/acmesh-official/acme.sh/wiki/deployhooks)
- Notification hooks: [notify](https://github.com/acmesh-official/acme.sh/wiki/notify)
- Options: [Options-and-Params](https://github.com/acmesh-official/acme.sh/wiki/Options-and-Params)
- **In-code usage**: add usage examples in the help text of `acme.sh` itself.
- **README**: add website URLs for new DNS providers.
---
## CI and Merge Hygiene
- All CI checks must pass before merge.
- Rebase to the latest `dev` branch frequently -- do not use merge commits.
- Enable GitHub Actions on your fork to catch errors early.
- Run the [DNS API Test](https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Test) workflow for DNS plugins.
- For Docker changes, ensure the Dockerfile includes any required dependencies.
---
## Debug Logging
- Use `_debug2` (not `_debug3` or other levels) unless there is a specific reason for a different level.
---
## Things to Avoid in Reviews
- Do not comment on purely stylistic issues like spacing or indentation, which should be handled by tools like ShellCheck or `shfmt`.
- Do not be overly verbose unless a significant issue is found. Keep feedback concise and actionable.

50
.github/workflows/Apache.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: Apache
on:
push:
paths:
- '*.sh'
- '.github/workflows/Apache.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Apache.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Apache:
runs-on: ubuntu-latest
env:
TestingDomain: example.com
TEST_ACME_Server: https://localhost:14000/dir
HTTPS_INSECURE: 1
TEST_LOCAL: 1
TEST_CA: "Pebble Intermediate CA"
TEST_APACHE: 1
CASE: le_test_apache
steps:
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat apache2
- name: Run Pebble
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
- name: Set up Pebble
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
- name: Set up Apache
# Apache serves on 5002, which is the HTTP-01 validation port in
# Pebble's default config; acme.sh appends the challenge Alias to
# the main config itself
run: |
echo "Listen 5002" | sudo tee /etc/apache2/ports.conf
sudo sed -i "s/\*:80/*:5002/" /etc/apache2/sites-available/000-default.conf
sudo apache2ctl configtest
sudo systemctl restart apache2
curl -s -o /dev/null -w "%{http_code}" -H "Host: example.com" http://127.0.0.1:5002/ | grep -E "200|403|404"
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
run: cd ../acmetest && sudo --preserve-env ./letest.sh

View file

@ -26,9 +26,9 @@ jobs:
id: step_one
run: |
if [ "${{secrets.TokenName1}}" ] ; then
echo "::set-output name=hasToken::true"
echo "hasToken=true" >> "$GITHUB_OUTPUT"
else
echo "::set-output name=hasToken::false"
echo "hasToken=false" >> "$GITHUB_OUTPUT"
fi
- name: Check the value
run: echo ${{ steps.step_one.outputs.hasToken }}
@ -66,7 +66,7 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Set env file
@ -114,9 +114,11 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install tools
run: brew install socat
run: |
brew untap aws/tap || true
brew install socat
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
@ -165,7 +167,7 @@ jobs:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install cygwin base packages with chocolatey
run: |
choco config get cacheLocation
@ -176,9 +178,14 @@ jobs:
C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git
shell: cmd
- name: Set ENV
shell: cmd
shell: bash
run: |
echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin >> %GITHUB_ENV%
echo 'PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin' >> "$GITHUB_ENV"
# cygwin git sees the runner workspace as owned by another user and
# fails with "dubious ownership" (exit 128) in the checkout post step
echo 'GIT_CONFIG_COUNT=1' >> "$GITHUB_ENV"
echo 'GIT_CONFIG_KEY_0=safe.directory' >> "$GITHUB_ENV"
echo 'GIT_CONFIG_VALUE_0=*' >> "$GITHUB_ENV"
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
@ -224,15 +231,17 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/freebsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg install -y socat curl
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@ -251,13 +260,19 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
OpenBSD:
GhostBSD:
runs-on: ubuntu-latest
needs: FreeBSD
# GhostBSD VM frequently flakes on boot/ssh; don't let it fail the whole run
continue-on-error: true
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
@ -275,15 +290,17 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openbsd-vm@v1
- uses: vmactions/ghostbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg_add socat curl libiconv
prepare: pkg install -y socat curl
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@ -302,9 +319,70 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
OpenBSD:
runs-on: ubuntu-latest
needs: GhostBSD
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg_add socat curl libiconv
usesh: true
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
NetBSD:
runs-on: ubuntu-latest
@ -326,16 +404,18 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/netbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: |
/usr/sbin/pkg_add curl socat
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@ -354,7 +434,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
@ -378,16 +462,19 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/dragonflybsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: |
pkg install -y curl socat libnghttp2
pkg install -y libnghttp2
pkg install -y curl socat
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@ -406,16 +493,77 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
MidnightBSD:
runs-on: ubuntu-latest
needs: DragonFlyBSD
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/midnightbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: mport install socat curl || true
usesh: true
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
Solaris:
runs-on: ubuntu-latest
needs: DragonFlyBSD
needs: MidnightBSD
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
@ -434,14 +582,18 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/solaris-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
copyback: false
prepare: pkgutil -y -i socat
sync: nfs
prepare: |
pkgutil -U
pkgutil -y -i socat
run: |
pkg set-mediator -v -I default@1.1 openssl
export PATH=/usr/gnu/bin:$PATH
@ -462,6 +614,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
Omnios:
@ -485,13 +642,15 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/omnios-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
copyback: false
sync: nfs
prepare: pkg install socat
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
@ -511,5 +670,306 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
OpenIndiana:
runs-on: ubuntu-latest
needs: Omnios
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since OpenIndiana doesn't accept the expired ISRG X1 root
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openindiana-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: pkg install socat
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
Tribblix:
runs-on: ubuntu-latest
needs: OpenIndiana
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since Tribblix doesn't accept the expired ISRG X1 root
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/tribblix-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: zap install socat
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
Haiku:
runs-on: ubuntu-latest
needs: Tribblix
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since OpenIndiana doesn't accept the expired ISRG X1 root
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/haiku-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: rsync
copyback: false
prepare: |
mkdir -p /boot/home/.cache
pkgman install -y cronie
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
Hurd:
runs-on: ubuntu-latest
needs: Haiku
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
HTTPS_INSECURE: 1 # always set to 1 to ignore https error
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/hurd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: rsync
copyback: false
usesh: true
prepare: |
apt-get update -y
apt-get install -y curl cron
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
OpenEuler:
runs-on: ubuntu-latest
needs: Hurd
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
HTTPS_INSECURE: 1 # always set to 1 to ignore https error
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openeuler-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: rsync
copyback: false
usesh: true
prepare: dnf install -y curl socat cronie tar gzip
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

View file

@ -31,8 +31,8 @@ jobs:
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
# CA: "ZeroSSL RSA Domain Secure Site CA"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
@ -45,8 +45,8 @@ jobs:
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v4
- uses: vmactions/cf-tunnel@v0
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
@ -57,15 +57,22 @@ jobs:
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/dragonflybsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: |
pkg install -y curl socat libnghttp2
pkg install -y libnghttp2
pkg install -y curl socat
usesh: true
copyback: false
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

View file

@ -37,8 +37,8 @@ jobs:
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
# CA: "ZeroSSL RSA Domain Secure Site CA"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
@ -51,8 +51,8 @@ jobs:
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v4
- uses: vmactions/cf-tunnel@v0
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
@ -63,14 +63,20 @@ jobs:
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/freebsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install -y socat curl wget
usesh: true
copyback: false
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

83
.github/workflows/GhostBSD.yml vendored Normal file
View file

@ -0,0 +1,83 @@
name: GhostBSD
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/GhostBSD.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/GhostBSD.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
GhostBSD:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
# GhostBSD VM frequently flakes on boot/ssh; don't let it fail the whole run
continue-on-error: true
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/ghostbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install -y socat curl wget
usesh: true
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

85
.github/workflows/Haiku.yml vendored Normal file
View file

@ -0,0 +1,85 @@
name: Haiku
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Haiku.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Haiku.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Haiku:
strategy:
fail-fast: false
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/haiku-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: |
mkdir -p /boot/home/.cache
pkgman install -y cronie
sync: rsync
copyback: false
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

76
.github/workflows/Hurd.yml vendored Normal file
View file

@ -0,0 +1,76 @@
name: Hurd
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Hurd.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Hurd.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Hurd:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
steps:
- uses: actions/checkout@v7
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/hurd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
nat: |
"8080": "80"
# Do NOT install socat: socat's SYSTEM: address is broken on GNU Hurd
# (the child shell output goes to socat's stdout instead of the socket,
# so clients get an empty reply). Without socat, acme.sh standalone
# mode falls back to its python3 server, which works on Hurd.
prepare: |
apt-get update -y
apt-get install -y curl cron
usesh: true
sync: rsync
copyback: false
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

View file

@ -33,7 +33,14 @@ jobs:
TEST_PREFERRED_CHAIN: (STAGING)
TEST_ACME_Server: "LetsEncrypt.org_test"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 80
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: |
cd .. \

View file

@ -31,8 +31,8 @@ jobs:
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
# CA: "ZeroSSL RSA Domain Secure Site CA"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: macos-latest
@ -44,9 +44,16 @@ jobs:
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install tools
run: brew install socat
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 80
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: |
cd .. \

75
.github/workflows/MidnightBSD.yml vendored Normal file
View file

@ -0,0 +1,75 @@
name: MidnightBSD
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/MidnightBSD.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/MidnightBSD.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
MidnightBSD:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/midnightbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: mport install socat curl wget || true
usesh: true
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

View file

@ -31,8 +31,8 @@ jobs:
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
# CA: "ZeroSSL RSA Domain Secure Site CA"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
@ -45,8 +45,8 @@ jobs:
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v4
- uses: vmactions/cf-tunnel@v0
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
@ -57,15 +57,21 @@ jobs:
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/netbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: |
/usr/sbin/pkg_add curl socat
usesh: true
copyback: false
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

66
.github/workflows/Nginx.yml vendored Normal file
View file

@ -0,0 +1,66 @@
name: Nginx
on:
push:
paths:
- '*.sh'
- '.github/workflows/Nginx.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Nginx.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Nginx:
runs-on: ubuntu-latest
env:
TestingDomain: example.com
TEST_ACME_Server: https://localhost:14000/dir
HTTPS_INSECURE: 1
TEST_LOCAL: 1
TEST_CA: "Pebble Intermediate CA"
TEST_NGINX: 1
CASE: le_test_nginx
steps:
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat nginx
- name: Run Pebble
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
- name: Set up Pebble
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
- name: Set up nginx
# a backend on 8081 plus a site with an aaPanel/BT style
# "location ^~ /" proxy block that shadows plain regex locations
# (regression for #6125); the site listens on 5002, which is the
# HTTP-01 validation port in Pebble's default config
run: |
sudo tee /etc/nginx/sites-available/default >/dev/null <<'EOF'
server {
listen 127.0.0.1:8081;
location / {
default_type text/plain;
return 200 "backend";
}
}
server {
listen 5002 default_server;
server_name example.com;
location ^~ / {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $http_host;
}
}
EOF
sudo nginx -t
sudo systemctl restart nginx
curl -s -H "Host: example.com" http://127.0.0.1:5002/ | grep backend
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
run: cd ../acmetest && sudo --preserve-env ./letest.sh

View file

@ -37,8 +37,8 @@ jobs:
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
# CA: "ZeroSSL RSA Domain Secure Site CA"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
@ -51,8 +51,8 @@ jobs:
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v4
- uses: vmactions/cf-tunnel@v0
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
@ -63,13 +63,19 @@ jobs:
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/omnios-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install socat wget
copyback: false
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

View file

@ -37,8 +37,8 @@ jobs:
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
# CA: "ZeroSSL RSA Domain Secure Site CA"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
@ -51,8 +51,8 @@ jobs:
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v4
- uses: vmactions/cf-tunnel@v0
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
@ -63,14 +63,20 @@ jobs:
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg_add socat curl wget libnghttp2
usesh: true
copyback: false
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

70
.github/workflows/OpenEuler.yml vendored Normal file
View file

@ -0,0 +1,70 @@
name: OpenEuler
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/OpenEuler.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/OpenEuler.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
OpenEuler:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
steps:
- uses: actions/checkout@v7
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openeuler-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
nat: |
"8080": "80"
prepare: dnf install -y curl socat cronie tar gzip
usesh: true
sync: rsync
copyback: false
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

81
.github/workflows/OpenIndiana.yml vendored Normal file
View file

@ -0,0 +1,81 @@
name: OpenIndiana
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/OpenIndiana.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/OpenIndiana.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
OpenIndiana:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openindiana-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install socat curl
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

View file

@ -33,7 +33,7 @@ jobs:
TEST_CA: "Pebble Intermediate CA"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat
- name: Run Pebble
@ -58,7 +58,7 @@ jobs:
TEST_IPCERT: 1
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat
- name: Run Pebble

View file

@ -37,8 +37,8 @@ jobs:
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
# CA: "ZeroSSL RSA Domain Secure Site CA"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
@ -51,8 +51,8 @@ jobs:
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v4
- uses: vmactions/cf-tunnel@v0
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
@ -63,13 +63,21 @@ jobs:
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/solaris-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkgutil -y -i socat curl wget
copyback: false
prepare: |
pkgutil -U
pkgutil -y -i socat curl wget
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

80
.github/workflows/Tribblix.yml vendored Normal file
View file

@ -0,0 +1,80 @@
name: Tribblix
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Tribblix.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Tribblix.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Tribblix:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v6
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/tribblix-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: zap install socat curl wget
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

View file

@ -37,8 +37,8 @@ jobs:
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
- TEST_ACME_Server: "ZeroSSL.com"
CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
CA: "ZeroSSL RSA Domain Secure Site CA"
CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
CA: "ZeroSSL RSA DV SSL CA 2"
CA_EMAIL: "githubtest@acme.sh"
TEST_PREFERRED_CHAIN: ""
- TEST_ACME_Server: "https://localhost:9000/acme/acme/directory"
@ -70,7 +70,7 @@ jobs:
TestingDomain: ${{ matrix.TestingDomain }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat wget
- name: Start StepCA

View file

@ -31,8 +31,8 @@ jobs:
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
# CA: "ZeroSSL RSA Domain Secure Site CA"
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
# CA: "ZeroSSL RSA DV SSL CA 2"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: windows-latest
@ -49,7 +49,7 @@ jobs:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install cygwin base packages with chocolatey
run: |
choco config get cacheLocation
@ -67,6 +67,13 @@ jobs:
shell: cmd
run: |
echo "PATH=%PATH%"
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 80
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
shell: cmd
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/

114
.github/workflows/blacklist-command.yml vendored Normal file
View file

@ -0,0 +1,114 @@
name: Blacklist Command
# An issue titled "blacklist: <login-or-email>" opened by the maintainer
# or a write-access member adds that identity to the Blacklist wiki page
# (see wiki-guard.yml) and closes the issue. The wiki-monitor notification
# embeds a prefilled link that opens such an issue in one click.
on:
issues:
types: [opened]
permissions:
contents: write
issues: write
# Share the wiki-guard concurrency group so we never push to the wiki
# at the same time as the guard.
concurrency:
group: wiki-guard
cancel-in-progress: false
jobs:
blacklist:
# Upstream only: forks have no <fork>.wiki repository to push to.
if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'blacklist:')
runs-on: ubuntu-latest
steps:
- name: Check authorization
id: auth
run: |
assoc="${{ github.event.issue.author_association }}"
case "$assoc" in
OWNER|MEMBER|COLLABORATOR)
echo "ok=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "issue author is not authorized ($assoc); ignoring"
echo "ok=false" >> "$GITHUB_OUTPUT"
;;
esac
- name: Checkout wiki repository
if: steps.auth.outputs.ok == 'true'
uses: actions/checkout@v7
with:
repository: ${{ github.repository }}.wiki
path: wiki
- name: Add the identity to the blacklist page
if: steps.auth.outputs.ok == 'true'
id: add
env:
TITLE: ${{ github.event.issue.title }}
run: |
target="$(printf '%s' "$TITLE" \
| sed 's/^blacklist:[[:space:]]*//; s/^@//; s/[[:space:]].*$//' \
| tr 'A-Z' 'a-z')"
case "$target" in
''|*[!a-z0-9._+@-]*)
echo "invalid target: '$target'"
echo "result=invalid" >> "$GITHUB_OUTPUT"
exit 0
;;
esac
echo "target=$target" >> "$GITHUB_OUTPUT"
cd wiki
if [ ! -e Blacklist.md ]; then
echo "result=nopage" >> "$GITHUB_OUTPUT"
exit 0
fi
if grep -Fxiq -- "- $target" Blacklist.md; then
echo "result=already" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -n "$(tail -c1 Blacklist.md)" ]; then
echo >> Blacklist.md
fi
printf -- '- %s\n' "$target" >> Blacklist.md
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Blacklist.md
git commit -m "blacklist $target (requested in #${{ github.event.issue.number }})"
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
echo "result=added" >> "$GITHUB_OUTPUT"
- name: Reply and close
if: steps.auth.outputs.ok == 'true'
uses: actions/github-script@v9
env:
RESULT: ${{ steps.add.outputs.result }}
TARGET: ${{ steps.add.outputs.target }}
with:
script: |
const result = process.env.RESULT;
const target = process.env.TARGET;
const messages = {
added: `\`${target}\` has been added to the [Blacklist](https://github.com/${context.repo.owner}/${context.repo.repo}/wiki/Blacklist). The wiki guard will revert their recent wiki changes on its next run.`,
already: `\`${target}\` is already on the blacklist.`,
invalid: "Could not parse a valid login or email from the issue title.",
nopage: "The Blacklist wiki page does not exist."
};
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: messages[result] || "No action taken."
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: "closed",
state_reason: result === "added" ? "completed" : "not_planned"
});

View file

@ -41,23 +41,29 @@ jobs:
runs-on: ubuntu-latest
needs: CheckToken
if: "contains(needs.CheckToken.outputs.hasToken, 'true')"
permissions:
contents: read
packages: write
steps:
- name: checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v4
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5.5.1
uses: docker/metadata-action@v6
with:
images: ${DOCKER_IMAGE}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v4
- name: login to docker hub
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: login to ghcr
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: build and push the image
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
@ -73,6 +79,8 @@ jobs:
fi
fi
echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV"
DOCKER_LABELS=()
while read -r label; do
DOCKER_LABELS+=(--label "${label}")
@ -84,3 +92,9 @@ jobs:
--output "type=image,push=true" \
--build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x .
- name: mirror the image to ghcr (best-effort)
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \
${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
|| echo "::warning::GHCR mirror failed; Docker Hub publish unaffected"

View file

@ -2,18 +2,128 @@ name: "Update issues"
on:
issues:
types: [opened]
issue_comment:
types: [created]
pull_request_target:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v9
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
const item = context.payload.issue || context.payload.pull_request;
// Close on sight anything opened by a user on the wiki Blacklist
// page (maintained by the Wiki Guard workflow).
let blacklist = [];
try {
const res = await fetch(`https://raw.githubusercontent.com/wiki/${context.repo.owner}/${context.repo.repo}/Blacklist.md`);
if (res.ok) {
blacklist = (await res.text()).split("\n")
.filter(l => l.startsWith("- "))
.map(l => l.slice(2).trim().toLowerCase())
.filter(Boolean);
}
} catch (e) {
core.warning(`Failed to fetch the blacklist: ${e}`);
}
// A comment on a closed tracking issue reopens it (the standard
// closing note promises this). Bots, blacklisted users and the
// maintainer's own comments don't reopen.
if (context.eventName === "issue_comment") {
const issue = context.payload.issue;
const commenter = context.payload.comment.user;
if (issue.pull_request || issue.state !== "closed") {
return;
}
if (!/^report\s+(bugs?|issues?)\b/i.test(issue.title)) {
return;
}
if (commenter.type === "Bot" ||
commenter.login.toLowerCase() === "neilpang" ||
blacklist.includes(commenter.login.toLowerCase())) {
return;
}
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "open"
});
return;
}
if (blacklist.includes(item.user.login.toLowerCase())) {
if (context.payload.pull_request) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: item.number,
state: "closed"
});
} else {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: item.number,
state: "closed",
state_reason: "not_planned"
});
}
return;
}
if (context.payload.pull_request) {
return;
}
const issue = context.payload.issue;
if (issue.title.startsWith("blacklist:") || issue.title.startsWith("revert:")) {
// Handled by the Blacklist / Revert Command workflows.
return;
}
if (/^report\s+(bugs?|issues?)\b/i.test(issue.title)) {
// Tracking issue for a third-party dns/deploy/notify api:
// no upgrade boilerplate; assign it to the opener, label it,
// then close it right away to keep the issue list clean. Any
// later comment reopens it (see the issue_comment handler).
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: [issue.user.login]
});
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ["3rd party api"]
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: "Closing this tracking issue for now to keep the issue list clean. It remains the place to report problems with this provider -- if you hit a bug, comment here and the issue will be reopened."
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "closed",
state_reason: "completed"
});
return;
}
await github.rest.issues.createComment({
issue_number: issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you."
body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you. Before posting the log, review it and REDACT any secrets: private keys (`-----BEGIN ... PRIVATE KEY-----` blocks), API tokens and passwords."
})

View file

@ -11,8 +11,9 @@ on:
jobs:
welcome:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v9
with:
script: |
await github.rest.issues.createComment({

View file

@ -13,8 +13,9 @@ on:
jobs:
welcome:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v9
with:
script: |
await github.rest.issues.createComment({

110
.github/workflows/revert-command.yml vendored Normal file
View file

@ -0,0 +1,110 @@
name: Revert Command
# An issue titled "revert: <wiki-commit-sha>" opened by the maintainer or
# a write-access member reverts that commit in the wiki repository and
# closes the issue. The wiki-monitor notification embeds a prefilled link
# that opens such an issue in one click.
on:
issues:
types: [opened]
permissions:
contents: write
issues: write
# Share the wiki-guard concurrency group so we never push to the wiki
# at the same time as the guard.
concurrency:
group: wiki-guard
cancel-in-progress: false
jobs:
revert:
# Upstream only: forks have no <fork>.wiki repository to push to.
if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'revert:')
runs-on: ubuntu-latest
steps:
- name: Check authorization
id: auth
run: |
assoc="${{ github.event.issue.author_association }}"
case "$assoc" in
OWNER|MEMBER|COLLABORATOR)
echo "ok=true" >> "$GITHUB_OUTPUT"
;;
*)
echo "issue author is not authorized ($assoc); ignoring"
echo "ok=false" >> "$GITHUB_OUTPUT"
;;
esac
- name: Checkout wiki repository
if: steps.auth.outputs.ok == 'true'
uses: actions/checkout@v7
with:
repository: ${{ github.repository }}.wiki
path: wiki
fetch-depth: 0
- name: Revert the wiki commit
if: steps.auth.outputs.ok == 'true'
id: revert
env:
TITLE: ${{ github.event.issue.title }}
run: |
target="$(printf '%s' "$TITLE" \
| sed 's/^revert:[[:space:]]*//; s/[[:space:]].*$//' \
| tr 'A-Z' 'a-z')"
case "$target" in
*[!0-9a-f]*|"")
echo "invalid commit sha: '$target'"
echo "result=invalid" >> "$GITHUB_OUTPUT"
exit 0
;;
esac
echo "target=$target" >> "$GITHUB_OUTPUT"
cd wiki
if ! git cat-file -e "$target^{commit}" 2>/dev/null; then
echo "result=notfound" >> "$GITHUB_OUTPUT"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git revert --no-edit "$target"; then
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
echo "result=reverted" >> "$GITHUB_OUTPUT"
else
git revert --abort || true
echo "result=conflict" >> "$GITHUB_OUTPUT"
fi
- name: Reply and close
if: steps.auth.outputs.ok == 'true'
uses: actions/github-script@v9
env:
RESULT: ${{ steps.revert.outputs.result }}
TARGET: ${{ steps.revert.outputs.target }}
with:
script: |
const result = process.env.RESULT;
const target = process.env.TARGET;
const messages = {
reverted: `Wiki commit \`${target}\` has been reverted.`,
conflict: `Reverting \`${target}\` conflicts with later edits; please revert manually from the page history.`,
notfound: `Commit \`${target}\` was not found in the wiki repository.`,
invalid: "Could not parse a commit sha from the issue title."
};
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: messages[result] || "No action taken."
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: "closed",
state_reason: result === "reverted" ? "completed" : "not_planned"
});

View file

@ -22,7 +22,7 @@ jobs:
ShellCheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Shellcheck
run: sudo apt-get install -y shellcheck
- name: DoShellcheck
@ -31,7 +31,7 @@ jobs:
shfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install shfmt
run: curl -sSL https://github.com/mvdan/sh/releases/download/v3.1.2/shfmt_v3.1.2_linux_amd64 -o ~/shfmt && chmod +x ~/shfmt
- name: shfmt

32
.github/workflows/vtag.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Mirror version tag
# Historical release tags are plain version numbers ("3.1.3") and cannot be
# renamed. When a plain version tag is pushed (including the tag created by
# publishing a GitHub release), mirror it as a "v"-prefixed tag ("v3.1.3")
# pointing to the same object, so both forms exist.
# No retrigger loop: the tag filter never matches a "v"-prefixed tag, and
# refs created with GITHUB_TOKEN do not fire workflows anyway.
on:
push:
tags:
- '[0-9]*'
permissions:
contents: write
jobs:
vtag:
if: github.repository == 'acmesh-official/acme.sh'
runs-on: ubuntu-latest
steps:
- name: Create the v-prefixed tag
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh api "repos/${{ github.repository }}/git/ref/tags/v${{ github.ref_name }}" >/dev/null 2>&1; then
echo "Tag v${{ github.ref_name }} already exists, nothing to do."
exit 0
fi
gh api "repos/${{ github.repository }}/git/refs" -f ref="refs/tags/v${{ github.ref_name }}" -f sha="${{ github.sha }}"
echo "Created tag v${{ github.ref_name }} -> ${{ github.sha }}"

325
.github/workflows/wiki-guard.yml vendored Normal file
View file

@ -0,0 +1,325 @@
name: Wiki Guard
# Rules enforced here:
# - Only the maintainer and write-access members may delete or rename wiki
# pages. Anyone else doing so gets blacklisted and the page restored to
# its last good revision.
# - Only the maintainer and write-access members may edit the Blacklist
# wiki page. Anyone else touching it gets blacklisted and the page
# reverted.
# - Any wiki change made by a blacklisted identity is reverted.
# A "good" revision is one authored by the maintainer, by this bot, or by
# a non-blacklisted user -- restoring from the deleted commit's parent is
# NOT safe, because vandals replace a page before destroying it and the
# parent would launder their version into a bot commit.
# The gollum event only fires on page create/update, never on deletion,
# so violations are caught by polling the wiki git history.
on:
schedule:
- cron: "*/10 * * * *"
gollum:
# Piggyback on frequent repo activity, because the cron schedule is
# best-effort and often delayed well beyond its interval.
issues:
types: [opened]
issue_comment:
types: [created]
workflow_dispatch:
permissions:
contents: write
issues: write
concurrency:
group: wiki-guard
cancel-in-progress: false
jobs:
guard:
# Forks have no <fork>.wiki repository, so the checkout below would
# fail there -- run only in the upstream repository.
if: github.repository == 'acmesh-official/acme.sh'
runs-on: ubuntu-latest
steps:
- name: Checkout wiki repository
uses: actions/checkout@v7
with:
repository: ${{ github.repository }}.wiki
path: wiki
fetch-depth: 0
- name: Enforce wiki rules
id: guard
env:
# WIKI_GUARD_TOKEN: a PAT with read:org, needed to enumerate
# members whose write access comes via the organization -- the
# repo-scoped GITHUB_TOKEN only sees direct collaborators.
GH_TOKEN: ${{ secrets.WIKI_GUARD_TOKEN || secrets.GITHUB_TOKEN }}
run: |
# Logins with write (push) access to the repository, including
# organization members -- they may delete/rename pages and edit
# the blacklist just like the maintainer. If the API call fails,
# the list stays empty and enforcement falls back to
# maintainer-only, which is the safe direction.
gh api "repos/${GITHUB_REPOSITORY}/collaborators?per_page=100" --paginate \
-q '.[] | select(.permissions.push) | .login' 2>/dev/null \
| tr 'A-Z' 'a-z' | sort -u > writers.txt || true
echo "write-access members loaded: $(wc -l < writers.txt)"
cd wiki
git config core.quotePath false
# Any author email under this domain is the maintainer.
OWNER_DOMAIN="neilpang.com"
# Our own enforcement commits.
BOT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com"
BL_PAGE="Blacklist.md"
# Rolling window; the cron runs every 10 minutes, so 7 days gives
# ample overlap without re-judging old changes the maintainer
# already accepted.
WINDOW="7 days ago"
: > ../actions.txt
: > ../bl_new.txt
is_owner() {
case "$1" in
*@"$OWNER_DOMAIN") return 0 ;;
esac
return 1
}
is_bot() {
[ "$1" = "$BOT_EMAIL" ]
}
author_email() {
git show -s --format=%ae "$1" | tr 'A-Z' 'a-z'
}
# Identity of a commit author: the GitHub login when the email is a
# users.noreply.github.com address, otherwise the email itself.
identity_of() {
case "$1" in
*+*@users.noreply.github.com)
printf '%s\n' "$1" | sed 's/^[^+]*+//; s/@users\.noreply\.github\.com$//'
;;
*@users.noreply.github.com)
printf '%s\n' "$1" | sed 's/@users\.noreply\.github\.com$//'
;;
*)
printf '%s\n' "$1"
;;
esac
}
is_blacklisted() {
grep -Fxq "$1" ../bl_all.txt
}
# Trusted committers: the maintainer (by email domain), this bot,
# and anyone whose GitHub login has write access to the repo.
is_trusted() {
if is_owner "$1" || is_bot "$1"; then
return 0
fi
grep -Fxq "$(identity_of "$1")" ../writers.txt
}
# Newest commit on file $1 authored by a non-blacklisted user.
last_good_for() {
for g in $(git log --format=%H --no-renames -- "$1"); do
gae="$(author_email "$g")"
if is_trusted "$gae"; then
printf '%s\n' "$g"
return 0
fi
gid="$(identity_of "$gae")"
if ! is_blacklisted "$gid" && ! is_blacklisted "$gae"; then
printf '%s\n' "$g"
return 0
fi
done
return 0
}
if [ -e "$BL_PAGE" ]; then
page_existed=1
else
page_existed=""
fi
# ---- 1. Last good version of the blacklist page: the newest
# revision authored by the maintainer or by this bot. Everything
# else on that page is tampering and is discarded.
bl_good_commit=""
for c in $(git log --format=%H --no-renames -- "$BL_PAGE"); do
ae="$(author_email "$c")"
if is_trusted "$ae"; then
bl_good_commit="$c"
break
fi
done
if [ -n "$bl_good_commit" ] && git cat-file -e "$bl_good_commit:$BL_PAGE" 2>/dev/null; then
git show "$bl_good_commit:$BL_PAGE" > ../bl_page.txt
else
{
echo "# Blacklist"
echo ""
echo "Users listed below violated the wiki rules (deleted or renamed"
echo "pages, or tampered with this page). Their new issues and pull"
echo "requests are closed on sight and their wiki edits are reverted"
echo "automatically. Only the maintainer and write-access members"
echo "may edit this page."
echo ""
echo "To pardon a user while their violation is still inside the"
echo "scan window, replace their entry with: pardon: username"
echo ""
} > ../bl_page.txt
fi
sed -n 's/^- *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_good.txt
sed -n 's/^[Pp]ardon: *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_pardon.txt
bl_add() {
if grep -Fxq "$1" ../bl_pardon.txt; then
return 0
fi
if ! grep -Fxq "$1" ../bl_good.txt && ! grep -Fxq "$1" ../bl_new.txt; then
printf '%s\n' "$1" >> ../bl_new.txt
printf '%s\n' "- blacklisted \`$1\`: $2" >> ../actions.txt
fi
}
# ---- 2. Blacklist everyone who deleted or renamed a page.
# --no-renames makes a rename count as a deletion of the old path.
for c in $(git log --since="$WINDOW" --diff-filter=D --no-renames --format=%H); do
ae="$(author_email "$c")"
if is_trusted "$ae"; then
continue
fi
an="$(git show -s --format=%an "$c")"
bl_add "$(identity_of "$ae")" "deleted or renamed pages in $c ($an <$ae>)"
done
# ---- 3. Blacklist everyone else who touched the blacklist page.
# The revert of their tampering falls out of steps 5 and 6.
for c in $(git log --since="$WINDOW" --format=%H --no-renames -- "$BL_PAGE"); do
ae="$(author_email "$c")"
if is_trusted "$ae"; then
continue
fi
an="$(git show -s --format=%an "$c")"
bl_add "$(identity_of "$ae")" "tampered with \`$BL_PAGE\` in $c ($an <$ae>)"
done
sort -u ../bl_new.txt > ../bl_new_u.txt
cat ../bl_good.txt ../bl_new_u.txt | sort -u > ../bl_all.txt
# ---- 4. Restore pages that are currently missing because a
# non-maintainer deleted them, using the last good revision.
git log --since="$WINDOW" --diff-filter=D --no-renames --name-only --format= \
| sort -u \
| while IFS= read -r f; do
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ] || [ -e "$f" ]; then
continue
fi
del="$(git log -1 --diff-filter=D --no-renames --format=%H -- "$f")"
if [ -z "$del" ]; then
continue
fi
ae="$(author_email "$del")"
if is_trusted "$ae"; then
continue
fi
good="$(last_good_for "$f")"
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
git checkout "$good" -- "$f"
printf '%s\n' "- restored \`$f\` (deleted in $del) from its last good revision $good" >> ../actions.txt
fi
done
# ---- 5. Revert every recent change made by a blacklisted
# identity: each touched file goes back to its newest revision
# authored by a non-blacklisted user; a file that has no such
# revision (they created it) is removed.
if [ -s ../bl_all.txt ]; then
for c in $(git log --since="$WINDOW" --format=%H --no-renames); do
ae="$(author_email "$c")"
if is_trusted "$ae"; then
continue
fi
id="$(identity_of "$ae")"
if ! is_blacklisted "$id" && ! is_blacklisted "$ae"; then
continue
fi
git show --name-only --no-renames --format= "$c" \
| while IFS= read -r f; do
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ]; then
continue
fi
good="$(last_good_for "$f")"
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
want="$(git rev-parse "$good:$f")"
have="$(git hash-object -- "$f" 2>/dev/null || echo missing)"
if [ "$want" != "$have" ]; then
git checkout "$good" -- "$f"
printf '%s\n' "- reverted \`$f\` to its last good revision $good (undoing change by \`$id\` in $c)" >> ../actions.txt
fi
elif [ -e "$f" ]; then
git rm -q -- "$f"
printf '%s\n' "- removed \`$f\` created by blacklisted \`$id\` in $c" >> ../actions.txt
fi
done
done
fi
# ---- 6. Regenerate the blacklist page: the last good text plus
# any newly blacklisted identities. This both reverts tampering
# and records new violators; manual edits by the maintainer are
# preserved as the new good text.
cp ../bl_page.txt ../bl_page_new.txt
if [ -s ../bl_page_new.txt ] && [ -n "$(tail -c1 ../bl_page_new.txt)" ]; then
echo >> ../bl_page_new.txt
fi
while IFS= read -r id; do
if [ -n "$id" ] && ! grep -Fxiq -- "- $id" ../bl_page_new.txt; then
printf -- '- %s\n' "$id" >> ../bl_page_new.txt
fi
done < ../bl_new_u.txt
if ! cmp -s ../bl_page_new.txt "$BL_PAGE" 2>/dev/null; then
cp ../bl_page_new.txt "$BL_PAGE"
git add -- "$BL_PAGE"
if [ -n "$page_existed" ] || [ -s ../bl_new_u.txt ]; then
printf '%s\n' "- updated \`$BL_PAGE\`" >> ../actions.txt
fi
fi
# ---- 7. Commit, push, notify.
if [ -n "$(git status --porcelain)" ]; then
git config user.name "github-actions[bot]"
git config user.email "$BOT_EMAIL"
git commit -m "wiki-guard: restore pages and enforce blacklist"
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
fi
if [ -s ../actions.txt ]; then
{
echo "The wiki guard handled the following rule violations:"
echo ""
cat ../actions.txt
echo ""
echo "Blacklist: https://github.com/${GITHUB_REPOSITORY}/wiki/Blacklist"
echo "Wiki: https://github.com/${GITHUB_REPOSITORY}/wiki"
} > ../guard-msg.txt
echo "acted=true" >> "$GITHUB_OUTPUT"
else
echo "No rule violations found."
echo "acted=false" >> "$GITHUB_OUTPUT"
fi
- name: Create issue to notify Neilpang
if: steps.guard.outputs.acted == 'true'
uses: peter-evans/create-issue-from-file@v6
with:
title: "Wiki guard: rule violations handled"
content-filepath: ./guard-msg.txt
assignees: Neilpang

View file

@ -6,15 +6,17 @@ on:
jobs:
notify:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- name: Checkout wiki repository
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
repository: ${{ github.repository }}.wiki
path: wiki
fetch-depth: 0
- name: Generate wiki change message
id: msg
run: |
actor="${{ github.actor }}"
sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH")
@ -22,9 +24,21 @@ jobs:
page_sha=$(jq -r '.pages[0].sha' "$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_summary=$(jq -r '.pages[0].summary' "$GITHUB_EVENT_PATH")
now="$(date '+%Y-%m-%d %H:%M:%S')"
cd wiki
# Skip notification when the change was authored by the
# maintainer himself (any author email under neilpang.com),
# e.g. a direct git push to the wiki repository.
author_email=$(git show -s --format=%ae "$page_sha" 2>/dev/null | tr 'A-Z' 'a-z')
case "$author_email" in
*@neilpang.com)
echo "Change authored by maintainer ($author_email); skipping notification."
echo "notify=false" >> "$GITHUB_OUTPUT"
exit 0
;;
esac
prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1)
if [ -n "$prev_sha" ]; then
git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff
@ -35,19 +49,25 @@ jobs:
{
echo "Wiki edited"
echo -n "User: "
echo "[$actor]($sender_url)"
echo "@$actor [$actor]($sender_url)"
echo "Time: $now"
echo "Page: [$page_name]($page_url) (Action: $page_action)"
echo "Comment: $page_summary"
echo "[Click here to Revert](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=revert%3A+${page_sha}&body=Revert+wiki+commit+${page_sha}+by+@${actor}.)"
echo ""
echo "[Click here to Blacklist @$actor](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=blacklist%3A+${actor}&body=Blacklist+@${actor},+requested+from+the+wiki+monitor.)"
echo ""
echo "----"
echo "### diff"
echo "### diff:"
echo '```diff'
cat wiki.diff
echo '```'
} > wiki-change-msg.txt
echo "notify=true" >> "$GITHUB_OUTPUT"
- name: Create issue to notify Neilpang
uses: peter-evans/create-issue-from-file@v5
if: steps.msg.outputs.notify == 'true'
uses: peter-evans/create-issue-from-file@v6
with:
title: "Wiki edited"
content-filepath: ./wiki-change-msg.txt

8
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,8 @@
# Contributing
1. Do NOT send pull request to `master` branch.
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/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.

View file

@ -1,4 +1,4 @@
FROM alpine:3.22
FROM alpine:3.23
RUN apk --no-cache add -f \
openssl \
@ -13,12 +13,15 @@ RUN apk --no-cache add -f \
tar \
libidn \
jq \
cronie
yq-go \
supercronic
ENV LE_WORKING_DIR=/acmebin
ENV LE_CONFIG_HOME=/acme.sh
ENV HOME=/acme.sh
ARG AUTO_UPGRADE=1
ENV AUTO_UPGRADE=$AUTO_UPGRADE
@ -29,10 +32,13 @@ COPY ./deploy /install_acme.sh/deploy
COPY ./dnsapi /install_acme.sh/dnsapi
COPY ./notify /install_acme.sh/notify
RUN addgroup -g 1000 acme && adduser -h $LE_CONFIG_HOME -s /bin/sh -G acme -D -H -u 1000 acme
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 $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh
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 chown -R acme:acme $LE_CONFIG_HOME
RUN for verb in help \
version \
@ -71,7 +77,15 @@ RUN for verb in help \
RUN printf "%b" '#!'"/usr/bin/env sh\n \
if [ \"\$1\" = \"daemon\" ]; then \n \
exec crond -n -s -m off \n \
if [ ! -f \"\$LE_CONFIG_HOME/crontab\" ]; then \n \
echo \"\$LE_CONFIG_HOME/crontab not found, generating one\" \n \
time=\$(date -u \"+%s\") \n \
random_minute=\$((\$time % 60)) \n \
random_hour=\$((\$time / 60 % 6)) \n \
echo \"\$random_minute \$random_hour,\$((\$random_hour + 6)),\$((\$random_hour + 12)),\$((\$random_hour + 18)) * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \
fi \n \
echo \"Running Supercronic using crontab at \$LE_CONFIG_HOME/crontab\" \n \
exec -- /usr/bin/supercronic \"\$LE_CONFIG_HOME/crontab\" \n \
else \n \
exec -- \"\$@\"\n \
fi\n" >/entry.sh && chmod +x /entry.sh && chmod -R o+rwx $LE_WORKING_DIR && chmod -R o+rwx $LE_CONFIG_HOME

522
README.md
View file

@ -1,54 +1,91 @@
[![zerossl.com](https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05)](https://zerossl.com/?fromacme.sh)
<p align="center">
<a href="https://zerossl.com?utm_source=acme-sh">
<picture>
<!-- Dark mode -->
<source
media="(prefers-color-scheme: dark)"
srcset="https://github.com/user-attachments/assets/1308516b-e0cc-496d-b5df-e3932423ead6" />
<!-- Light mode -->
<source
media="(prefers-color-scheme: light)"
srcset="https://github.com/user-attachments/assets/4ba7a79e-8cc9-4d49-87fc-02d44fb7b043" />
<!-- Fallback for environments without media queries -->
<img
alt="ZeroSSL"
src="https://github.com/user-attachments/assets/4ba7a79e-8cc9-4d49-87fc-02d44fb7b043"
height="auto" />
</picture>
</a>
</p>
# An ACME Shell script: acme.sh
<h1 align="center">🔐 acme.sh</h1>
<h3 align="center">An ACME Protocol Client Written Purely in Shell</h3>
[![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)
[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)
<p align="center">
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg" alt="FreeBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg" alt="OpenBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg" alt="NetBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg" alt="MacOS"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg" alt="Ubuntu"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg" alt="Windows"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg" alt="Solaris"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg" alt="DragonFlyBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml/badge.svg" alt="MidnightBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg" alt="GhostBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg" alt="Omnios"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg" alt="OpenIndiana"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg" alt="Tribblix"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg" alt="Haiku"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg" alt="Hurd"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg" alt="OpenEuler"></a>
</p>
![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)
<p align="center">
<img src="https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg" alt="Shellcheck">
<img src="https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg" alt="PebbleStrict">
<img src="https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg" alt="DockerHub">
</p>
<p align="center">
<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/all/badge.svg?label=financial+contributors" alt="Financial Contributors on Open Collective"></a>
<a href="https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/acme-sh/Lobby.svg" alt="Join the chat at Gitter"></a>
<a href="https://hub.docker.com/r/neilpang/acme.sh" title="Click to view the image on Docker Hub"><img src="https://img.shields.io/docker/stars/neilpang/acme.sh.svg" alt="Docker stars"></a>
<a href="https://hub.docker.com/r/neilpang/acme.sh" title="Click to view the image on Docker Hub"><img src="https://img.shields.io/docker/pulls/neilpang/acme.sh.svg" alt="Docker pulls"></a>
</p>
<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")
---
## ✨ Features
- 🐚 An ACME protocol client written **purely in Shell** (Unix shell) language
- 📜 Full ACME protocol implementation
- 🔑 Support **ECDSA** certificates
- 🌐 Support **SAN** and **wildcard** certificates
- ⚡ Simple, powerful and very easy to use — only **3 minutes** to learn!
- 🔧 Compatible with **Bash**, **dash** and **sh**
- 🚫 No dependencies on Python
- 🔄 One script to issue, renew and install your certificates automatically
- 👤 **DOES NOT** require `root/sudoer` access
- 🐳 Docker ready
- 🌍 IPv6 ready
- 📧 Cron job notifications for renewal or error
- An ACME protocol client written purely in Shell (Unix shell) language.
- Full ACME protocol implementation.
- 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.
- 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 ready
- IPv6 ready
- Cron job notifications for renewal or error etc.
> 💡 It's probably the **easiest & smartest** shell script to automatically issue & renew free certificates.
It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates.
<p align="center">
<a href="https://github.com/acmesh-official/acme.sh/wiki"><strong>📚 Wiki</strong></a>
<a href="https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker"><strong>🐳 Docker Guide</strong></a>
<a href="https://twitter.com/neilpangxa"><strong>🐦 Twitter</strong></a>
</p>
Wiki: https://github.com/acmesh-official/acme.sh/wiki
---
For Docker Fans: [acme.sh :two_hearts: Docker ](https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker)
## 🌏 [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
---
# [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
# Who:
## 🏆 Who Uses acme.sh?
- [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/Certificate_Management)
@ -62,7 +99,9 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
- [lnmp.org](https://lnmp.org/)
- [more...](https://github.com/acmesh-official/acme.sh/wiki/Blogs-and-tutorials)
# Tested OS
---
## 🖥️ Tested OS
| NO | Status| Platform|
|----|-------|---------|
@ -75,66 +114,83 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|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|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios
|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)| Debian
|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)|CentOS
|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)|openSUSE
|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)|Alpine Linux (with curl)
|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)|Archlinux
|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)|fedora
|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)|Kali 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
|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|[![MidnightBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml)|MidnightBSD
|11|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios
|12|[![OpenIndiana](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml)|OpenIndiana
|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)| Debian
|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)|openSUSE
|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)|Alpine Linux (with curl)
|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)|Archlinux
|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)|fedora
|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)|Kali 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)|Oracle Linux
|20|[![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
|21|[![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
|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)
|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)
|25|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
|26|[![Tribblix](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
|27|[![GhostBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
|28|[![Hurd](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd
|29|[![OpenEuler](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml)|openEuler
Check our [testing project](https://github.com/acmesh-official/acmetest):
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
>
> 🖥️ The testing VMs are supported by [vmactions.org](https://vmactions.org)
https://github.com/acmesh-official/acmetest
---
# Supported CA
## 🏛️ Supported CA
- [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA)(default)
- Letsencrypt.org 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)
- [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA)
- [Pebble strict Mode](https://github.com/letsencrypt/pebble)
- Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA
| CA | Status |
|---|---|
| [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA) | ⭐ **Default** |
| Letsencrypt.org CA | ✅ Supported |
| [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) | ✅ Supported |
| [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) | ✅ Supported |
| [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA) | ✅ Supported |
| [Pebble strict Mode](https://github.com/letsencrypt/pebble) | ✅ Supported |
| Any [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA | ✅ Supported |
# Supported modes
---
- Webroot mode
- Standalone mode
- Standalone tls-alpn mode
- Apache mode
- Nginx mode
- DNS 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)
## ⚙️ Supported Modes
| Mode | Description |
|------|-------------|
| 📁 Webroot mode | Use existing webroot directory |
| 🖥️ Standalone mode | Built-in webserver on port 80 |
| 🔐 Standalone tls-alpn mode | Built-in webserver on port 443 |
| 🪶 Apache mode | Use Apache for verification |
| ⚡ Nginx mode | Use Nginx for verification |
| 🌐 DNS mode | Use DNS TXT records |
| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification |
| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification |
| 📌 [DNS persist mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode) | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) |
# 1. How to install
---
### 1. Install online
## 📖 Usage Guide
Check this project: https://github.com/acmesh-official/get.acme.sh
### 1⃣ How to Install
#### 📥 Install Online
> Check this project: https://github.com/acmesh-official/get.acme.sh
```bash
curl https://get.acme.sh | sh -s email=my@example.com
```
Or:
**Or:**
```bash
wget -O - https://get.acme.sh | sh -s email=my@example.com
```
### 2. Or, Install from git
#### 📦 Install from Git
Clone this project and launch installation:
@ -144,11 +200,11 @@ cd ./acme.sh
./acme.sh --install -m my@example.com
```
You `don't have to be root` then, although `it is recommended`.
> 💡 You `don't have to be root` then, although `it is recommended`.
Advanced Installation: https://github.com/acmesh-official/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:
**The installer will perform 3 actions:**
1. Create and copy `acme.sh` to your home dir (`$HOME`): `~/.acme.sh/`.
All certs will be placed in this folder too.
@ -161,17 +217,19 @@ Cron entry example:
0 0 * * * "/home/user/.acme.sh"/acme.sh --cron --home "/home/user/.acme.sh" > /dev/null
```
After the installation, you must close the current terminal and reopen it to make the alias take effect.
> ⚠️ After the installation, you must close the current terminal and reopen it to make the alias take effect.
Ok, you are ready to issue certs now.
✅ **You are ready to issue certs now!**
Show help message:
**Show help message:**
```sh
root@v1:~# acme.sh -h
acme.sh -h
```
# 2. Just issue a cert
---
### 2⃣ Issue a Certificate
**Example 1:** Single domain.
@ -206,19 +264,21 @@ You must point and bind all the domains to the same webroot dir: `/home/wwwroot/
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 **30** days.
The certs will default to ECC certificates.
> 🔐 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
---
# 3. Install the cert to Apache/Nginx etc.
### 3⃣ Install the Certificate to Apache/Nginx
After the cert is generated, you probably want to install/copy the cert to your Apache/Nginx or other servers.
You **MUST** use this command to copy the certs to the target files, **DO NOT** use the certs files in **~/.acme.sh/** folder, they are for internal use only, the folder structure may change in the future.
**Apache** example:
> ⚠️ **IMPORTANT:** You **MUST** use this command to copy the certs to the target files. **DO NOT** use the certs files in `~/.acme.sh/` folder — they are for internal use only, the folder structure may change in the future.
#### 🪶 Apache Example:
```bash
acme.sh --install-cert -d example.com \
--cert-file /path/to/certfile/in/apache/cert.pem \
@ -227,7 +287,7 @@ acme.sh --install-cert -d example.com \
--reloadcmd "service apache2 force-reload"
```
**Nginx** example:
#### ⚡ Nginx Example:
```bash
acme.sh --install-cert -d example.com \
--key-file /path/to/keyfile/in/nginx/key.pem \
@ -241,91 +301,89 @@ The ownership and permission info of existing files are preserved. You can pre-c
Install/copy the cert/key to the production Apache or Nginx path.
The cert will be renewed every **60** days by default (which is configurable). Once the cert is renewed, the Apache/Nginx service will be reloaded automatically by the command: `service apache2 force-reload` or `service nginx force-reload`.
> 🔄 The cert will be renewed every **30** days by default (configurable). Once renewed, the Apache/Nginx service will be reloaded automatically.
> ⚠️ **IMPORTANT:** The `reloadcmd` is very important. The cert can be automatically renewed, but without a correct `reloadcmd`, the cert may not be flushed to your server (like nginx or apache), then your website will not be able to show the renewed cert.
**Please take care: The reloadcmd is very important. The cert can be automatically renewed, but, without a correct 'reloadcmd' the cert may not be flushed to your server(like nginx or apache), then your website will not be able to show renewed cert in 60 days.**
---
# 4. Use Standalone server to issue cert
### 4⃣ Use Standalone Server to Issue Certificate
**(requires you to be root/sudoer or have permission to listen on port 80 (TCP))**
> 🔐 Requires root/sudoer or permission to listen on port **80** (TCP)
Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
> ⚠️ Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
```bash
acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com
```
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
# 5. Use Standalone ssl server to issue cert
---
**(requires you to be root/sudoer or have permission to listen on port 443 (TCP))**
### 5⃣ Use Standalone TLS Server to Issue Certificate
Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
> 🔐 Requires root/sudoer or permission to listen on port **443** (TCP)
> ⚠️ Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
```bash
acme.sh --issue --alpn -d example.com -d www.example.com -d cp.example.com
```
More examples: https://github.com/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
---
# 6. Use Apache mode
### 6⃣ Use Apache Mode
**(requires you to be root/sudoer, since it is required to interact with Apache server)**
> 🔐 Requires root/sudoer to interact with Apache server
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.
Just set string "apache" as the second argument and it will force use of apache plugin automatically.
```sh
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 with your apache server, don't worry.**
> 💡 **Note:** 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 with your Apache server, don't worry!
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
# 7. Use Nginx mode
---
**(requires you to be root/sudoer, since it is required to interact with Nginx server)**
### 7⃣ Use Nginx Mode
> 🔐 Requires root/sudoer to interact with Nginx server
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.
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.
Just set string "nginx" as the second argument.
It will configure nginx server automatically to verify the domain and then restore the nginx config to the original version.
So, the config is not changed.
It will configure Nginx server automatically to verify the domain and then restore the Nginx config to the original version. So, the config is not changed.
```sh
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 with your nginx server, don't worry.**
> 💡 **Note:** 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 with your Nginx server, don't worry!
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
# 8. Automatic DNS API integration
---
### 8⃣ Automatic DNS API Integration
If your DNS provider supports API access, we can use that API to automatically issue the certs.
You don't have to do anything manually!
> ✨ **You don't have to do anything manually!**
### Currently acme.sh supports most of the dns providers:
📚 **Currently acme.sh supports most DNS providers:** https://github.com/acmesh-official/acme.sh/wiki/dnsapi
https://github.com/acmesh-official/acme.sh/wiki/dnsapi
---
# 9. Use DNS manual mode:
### 9⃣ Use DNS Manual Mode
See: https://github.com/acmesh-official/acme.sh/wiki/dns-manual-mode first.
@ -355,67 +413,154 @@ Then just rerun with `renew` argument:
acme.sh --renew -d example.com
```
Ok, it's done.
✅ **Done!**
**Take care, this is dns manual mode, it can not be renewed automatically. you will have to add a new txt record to your domain by your hand when you renew your cert.**
> ⚠️ **WARNING:** This is DNS manual mode — it **cannot** be renewed automatically. You will have to add a new TXT record to your domain manually when you renew your cert. **Please use DNS API mode instead.**
**Please use dns api mode instead.**
---
# 10. Issue certificates of different key types and lengths (ECC or RSA)
### 🔟 Use DNS Persist Mode
Just set the `keylength` to a valid, supported, value.
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode
Valid values for the `keylength` parameter are:
📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)
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)**
DNS persist mode lets you place a **single, longlived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals.
For example:
#### 🪄 Step 1: Print the TXT record value
### Single domain with ECDSA P-384 certificate
```bash
acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] [--dns-persist-days 365]
```
Options:
| Flag | Description |
|------|-------------|
| `--server <ca>` | Pick the CA (default is your configured default). The account is registered automatically if you have not used this CA before. |
| `--dns-persist-wildcard` | Adds `policy=wildcard` to the record so it also authorizes wildcard / subdomain certs. |
| `--dns-persist-ca-name <name>` | Use a specific CA identity domain (e.g. `sectigo.com`). If omitted, identities are read from the ACME directory's `caaIdentities` field and one record per identity is printed — you only need to add **any one** of them. |
| `--dns-persist-days <N>` | Adds `persistUntil=<unix-timestamp>` to the record, set to N days from now. The CA will refuse new validations against the record after that time. Omit for a record with no expiry. |
You should get an output like:
```sh
TXT persist domain:_validation-persist.example.com
TXT persist value :"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789"
```
#### ✍️ Step 2: Add the TXT record to your DNS
Add the printed `TXT persist domain` / `TXT persist value` pair as a TXT record at your DNS provider, then wait for it to propagate.
#### 📜 Step 3: Issue the certificate
```bash
acme.sh --issue -d example.com --dns-persist
```
**Done!** No challenge token is provisioned during issuance — the CA reads the persistent TXT record directly.
> 🔄 Renewals just work: `acme.sh --renew -d example.com` (or the cron job) reuses the same TXT record automatically — no further DNS edits needed.
---
### 1⃣1⃣ Issue Certificates of Different Key Types (ECC or RSA)
Just set the `keylength` to a valid, supported value.
**Valid values for the `keylength` parameter:**
| Key Length | Description |
|------------|-------------|
| `ec-256` | prime256v1, "ECDSA P-256" ⭐ **Default** |
| `ec-384` | secp384r1, "ECDSA P-384" |
| `ec-521` | secp521r1, "ECDSA P-521" ⚠️ Not supported by Let's Encrypt yet |
| `2048` | RSA 2048-bit |
| `3072` | RSA 3072-bit |
| `4096` | RSA 4096-bit |
**Examples:**
#### Single domain with ECDSA P-384 certificate
```bash
acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-384
```
### SAN multi domain with RSA4096 certificate
#### SAN multi domain with RSA4096 certificate
```bash
acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096
```
# 11. Issue Wildcard certificates
---
It's simple, just give a wildcard domain as the `-d` parameter.
### 1⃣2⃣ Issue Wildcard Certificates
It's simple! Just give a wildcard domain as the `-d` parameter:
```sh
acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
```
# 12. How to renew the certs
---
No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days.
### 1⃣3⃣ How to Renew Certificates
However, you can also force to renew a cert:
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days, **or earlier when the CA's ARI says so** (see below).
However, you can force a renewal:
```sh
acme.sh --renew -d example.com --force
```
or, for ECC cert:
**For ECC cert:**
```sh
acme.sh --renew -d example.com --force --ecc
```
#### 📡 ACME Renewal Information (ARI) — RFC 9773
# 13. How to stop cert renewal
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/ARI
If the CA exposes a `renewalInfo` endpoint in its ACME directory (Let's Encrypt, ZeroSSL, etc.), `acme.sh` follows [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html) automatically — **no flag needed, no opt-in**:
| What | When | Why |
|------|------|-----|
| 🔍 **Polls `suggestedWindow`** | Every cron run, before deciding to skip | Lets the CA shift the renewal time forward in case of an incident (key compromise, mass revocation, etc.) |
| 🎯 **Picks a random renewal time** inside the window | Right after a successful issuance/renewal | Disperses renewals across the network so all clients don't hit the CA at the same instant |
| 🔗 **Sends `replaces=<certID>`** in `newOrder` | On renewal | Lets the CA correlate the new order with the certificate it supersedes (RFC 9773 §5) |
| ↩️ **Retries without `replaces`** | If the CA rejects with `alreadyReplaced` or an ARI validation error | Robust against edge cases (e.g. switching CAs, retired issuers) |
**Renewal trigger logic:** the cert is renewed if **any one** of the following becomes true:
1. `--force` is given
2. The CA's **ARI `suggestedWindow` has started**
3. The cached `Le_NextRenewTime` has passed (default fallback for CAs without ARI)
You can see the resulting next renewal time (already ARI-picked when applicable) in:
```sh
acme.sh --info -d example.com
# Look for: Le_NextRenewTimeStr=...
```
For the live ARI window the CA is currently advertising, run with `--debug 2`:
```sh
acme.sh --renew -d example.com --debug 2 2>&1 | grep -i 'ARI suggestedWindow'
```
> 💡 If your CA does not advertise `renewalInfo`, `acme.sh` falls back to the classic 30-day rule — no behavior change.
---
### 1⃣4⃣ How to Stop Certificate Renewal
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
@ -425,73 +570,80 @@ acme.sh --remove -d example.com [--ecc]
The cert/key file is not removed from the disk.
You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself.
> 💡 You can remove the respective directory (e.g. `~/.acme.sh/example.com`) manually.
---
# 14. How to upgrade `acme.sh`
### 1⃣5⃣ How to Upgrade acme.sh
acme.sh is in constant development, so it's strongly recommended to use the latest code.
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
You can update acme.sh to the latest code:
**Update to latest:**
```sh
acme.sh --upgrade
```
You can also enable auto upgrade:
**Enable auto upgrade:**
```sh
acme.sh --upgrade --auto-upgrade
```
Then **acme.sh** will be kept up to date automatically.
Disable auto upgrade:
**Disable auto upgrade:**
```sh
acme.sh --upgrade --auto-upgrade 0
```
---
# 15. Issue a cert from an existing CSR
### 1⃣6⃣ Issue a Certificate from an Existing CSR
https://github.com/acmesh-official/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
### 1⃣7⃣ Send Notifications in Cronjob
https://github.com/acmesh-official/acme.sh/wiki/notify
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
---
# 17. Under the Hood
### 1⃣8 Under the Hood
Speak ACME language using shell, directly to "Let's Encrypt".
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
TODO:
---
### 1⃣9⃣ Acknowledgments
# 18. Acknowledgments
| Project | Link |
|---------|------|
| 🙏 Acme-tiny | https://github.com/diafygi/acme-tiny |
| 📜 ACME protocol | https://github.com/ietf-wg-acme/acme |
1. Acme-tiny: https://github.com/diafygi/acme-tiny
2. ACME protocol: https://github.com/ietf-wg-acme/acme
---
## 👥 Contributors
## Contributors
### Code 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
If you want to become a contributor make sure to read [CONTRIBUTING.md](./CONTRIBUTING.md).
### 💰 Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)]
#### Individuals
#### 👤 Individuals
<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/individuals.svg?width=890"></a>
#### Organizations
#### 🏢 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)]
@ -506,25 +658,31 @@ Support this project with your organization. Your logo will show up here with 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>
---
### 2⃣0⃣ License & Others
# 19. License & Others
📄 **License:** GPLv3
License is GPLv3
⭐ Please **Star** and **Fork** this project!
Please Star and Fork me.
🐛 [Issues](https://github.com/acmesh-official/acme.sh/issues) and 🔀 [Pull Requests](https://github.com/acmesh-official/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.
---
### 2⃣1⃣ Donate
# 20. Donate
Your donation makes **acme.sh** better:
> 💝 Your donation makes **acme.sh** better!
1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)
| Method | Link |
|--------|------|
| 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
---
### 2⃣2⃣ 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!
@ -532,7 +690,7 @@ Your donation makes **acme.sh** better:
>
> 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">
<a href="https://zerossl.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">

1613
acme.sh

File diff suppressed because it is too large Load diff

341
acme.sh.completion Normal file
View file

@ -0,0 +1,341 @@
# Bash completion for acme.sh: https://github.com/acmesh-official/acme.sh
#
# "acme.sh --install" copies this file to the acme.sh home dir and wires
# it into acme.sh.env, so the completion is loaded automatically in new
# bash sessions after installation.
#
# To use it without installing acme.sh, source it from ~/.bashrc, or copy
# it to /usr/share/bash-completion/completions/acme.sh
#
# Zsh users can load it with:
# autoload -U +X bashcompinit && bashcompinit
# . /path/to/acme.sh.completion
# This file may also be sourced by non-bash shells via acme.sh.env,
# so silently do nothing if the "complete" builtin is not available.
if ! command -v complete >/dev/null 2>&1; then
return 0 2>/dev/null || exit 0
fi
# Add each word of $1 that starts with $cur to COMPREPLY.
# The words are read line by line, so that candidates like a wildcard
# domain "*.example.com" are never glob-expanded against the cwd.
_acme_sh_add_matches() {
local _word
while read -r _word; do
[ -n "$_word" ] || continue
case "$_word" in
"$cur"*) COMPREPLY=("${COMPREPLY[@]}" "$_word") ;;
esac
done <<EOF
$(printf '%s\n' "$1" | tr ' ' '\n')
EOF
return 0
}
_acme_sh_files() {
local _file
while IFS= read -r _file; do
[ -n "$_file" ] || continue
COMPREPLY=("${COMPREPLY[@]}" "$_file")
done <<EOF
$(compgen -f -- "$cur")
EOF
if command -v compopt >/dev/null 2>&1; then
compopt -o filenames 2>/dev/null
fi
return 0
}
_acme_sh_dirs() {
local _dir
while IFS= read -r _dir; do
[ -n "$_dir" ] || continue
COMPREPLY=("${COMPREPLY[@]}" "$_dir")
done <<EOF
$(compgen -d -- "$cur")
EOF
if command -v compopt >/dev/null 2>&1; then
compopt -o filenames 2>/dev/null
fi
return 0
}
# Complete the domains that already have a cert: every directory in the
# config home that contains a "<domain>.conf" file ("_ecc" suffix stripped).
_acme_sh_domains() {
local _dir _name _domains=""
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
for _dir in "$_acme_conf_home"/*/; do
[ -d "$_dir" ] || continue
_name="${_dir%/}"
_name="${_name##*/}"
_name="${_name%_ecc}"
if [ -f "${_dir}${_name}.conf" ]; then
case " $_domains " in
*" $_name "*) ;;
*) _domains="$_domains $_name" ;;
esac
fi
done
_acme_sh_add_matches "$_domains"
}
# Complete hook names from a subfolder of the acme.sh home dir.
# $1: subfolder (dnsapi/deploy/notify), $2: file name prefix or empty.
_acme_sh_hooks() {
local _file _hooks=""
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
for _file in "$_acme_home/$1/$2"*.sh; do
[ -f "$_file" ] || continue
_file="${_file##*/}"
_hooks="$_hooks ${_file%.sh}"
done
_acme_sh_add_matches "$_hooks"
}
_acme_sh_completion() {
local cur prev _acme_home _acme_conf_home
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev=""
if [ "$COMP_CWORD" -gt 0 ]; then
prev="${COMP_WORDS[COMP_CWORD - 1]}"
fi
_acme_home="${LE_WORKING_DIR:-$HOME/.acme.sh}"
_acme_conf_home="${LE_CONFIG_HOME:-$_acme_home}"
# The first argument is the command.
if [ "$COMP_CWORD" -eq 1 ]; then
_acme_sh_add_matches "
--help
--version
--install
--install-online
--uninstall
--upgrade
--issue
--deploy
--sign-csr
--show-csr
--install-cert
--renew
--renew-all
--revoke
--remove
--list
--list-profiles
--info
--to-pkcs12
--to-pkcs8
--create-account-key
--create-domain-key
--create-csr
--deactivate
--update-account
--register-account
--deactivate-account
--make-dns-persist-value
--install-cronjob
--uninstall-cronjob
--cron
--set-notify
--set-default-ca
--set-default-chain
"
return 0
fi
# Complete the value of the previous option.
case "$prev" in
-d | --domain | --challenge-alias | --domain-alias)
_acme_sh_domains
return 0
;;
--dns)
# The dns hook argument is optional, keep completing options if the
# current word already looks like one.
case "$cur" in
-*) ;;
*)
_acme_sh_hooks "dnsapi" "dns_"
return 0
;;
esac
;;
--deploy-hook)
_acme_sh_hooks "deploy" ""
return 0
;;
--notify-hook)
_acme_sh_hooks "notify" ""
return 0
;;
--server)
_acme_sh_add_matches "letsencrypt letsencrypt_test zerossl sslcom google google_test actalis"
return 0
;;
-k | --keylength | -ak | --accountkeylength)
_acme_sh_add_matches "2048 3072 4096 8192 ec-256 ec-384 ec-521"
return 0
;;
--debug)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_add_matches "0 1 2 3"
return 0
;;
esac
;;
--log)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_files
return 0
;;
esac
;;
--nginx)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_files
return 0
;;
esac
;;
--auto-upgrade | --always-force-new-domain-key)
# Optional argument.
case "$cur" in
-*) ;;
*)
_acme_sh_add_matches "0 1"
return 0
;;
esac
;;
--log-level)
_acme_sh_add_matches "1 2"
return 0
;;
--syslog)
_acme_sh_add_matches "0 3 6 7"
return 0
;;
--notify-level)
_acme_sh_add_matches "0 1 2 3"
return 0
;;
--notify-mode)
_acme_sh_add_matches "0 1"
return 0
;;
--revoke-reason)
_acme_sh_add_matches "0 1 2 3 4 5 6 7 8 9 10"
return 0
;;
--cert-file | --key-file | --ca-file | --fullchain-file | --csr | --accountconf | --accountkey | --ca-bundle | --openssl-bin)
_acme_sh_files
return 0
;;
-w | --webroot | --home | --cert-home | --config-home | --ca-path)
_acme_sh_dirs
return 0
;;
-m | --email | --password | --useragent | --days | --valid-from | --valid-to | --httpport | --tlsport | --local-address | --dnssleep | --pre-hook | --post-hook | --renew-hook | --reloadcmd | --extended-key-usage | -b | --branch | --notify-source | --eab-kid | --eab-hmac-key | --preferred-chain | --cert-profile | --certificate-profile | --dns-persist-ca-name | --dns-persist-days)
# These options take a free-form value, offer nothing.
return 0
;;
esac
# Complete the parameters.
_acme_sh_add_matches "
--accountconf
--accountkey
--accountkeylength
--alpn
--always-force-new-domain-key
--apache
--auto-upgrade
--branch
--ca-bundle
--ca-file
--ca-path
--cert-file
--cert-home
--cert-profile
--challenge-alias
--config-home
--csr
--days
--debug
--deploy-hook
--dns
--dns-persist
--dns-persist-ca-name
--dns-persist-days
--dns-persist-wildcard
--dnssleep
--domain
--domain-alias
--eab-hmac-key
--eab-kid
--ecc
--email
--extended-key-usage
--force
--force-color
--fullchain-file
--home
--httpport
--insecure
--key-file
--keylength
--listen-v4
--listen-v6
--listraw
--local-address
--log
--log-level
--nginx
--no-color
--no-cron
--no-profile
--notify-hook
--notify-level
--notify-mode
--notify-source
--ocsp-must-staple
--openssl-bin
--output-insecure
--password
--post-hook
--pre-hook
--preferred-chain
--reloadcmd
--renew-hook
--revoke-reason
--server
--staging
--standalone
--stateless
--stop-renew-on-error
--syslog
--tlsport
--treat-skip-as-success
--use-wget
--useragent
--valid-from
--valid-to
--webroot
--yes-I-know-dns-manual-mode-enough-go-ahead-please
"
return 0
}
complete -F _acme_sh_completion acme.sh

View file

@ -83,6 +83,6 @@ _set_cdn_domain_ssl_certificate_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2018-05-10'
}

View file

@ -83,6 +83,6 @@ _set_dcdn_domain_ssl_certificate_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2018-01-15'
}

222
deploy/baidu_cdn.sh Normal file
View file

@ -0,0 +1,222 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034,SC2154
# Deploy hook: Baidu Cloud CDN
#
# Code generated by GitHub Copilot with Claude Sonnet 4.6 and OpenAI Codex with GPT-5.6 Sol
#
# API Doc: https://cloud.baidu.com/doc/CDN/s/Zkna2r57w
#
# Uses the same credential variables as dnsapi/dns_baidu.sh:
# export Baidu_AK="your-access-key-id"
# export Baidu_SK="your-secret-access-key"
#
# To deploy to a CDN domain different from the certificate CN
# (e.g. wildcard or multi-domain certs):
# export DEPLOY_BAIDU_CDN_DOMAIN="cdn.example.com"
#
# Multiple CDN domains sharing the same certificate:
# export DEPLOY_BAIDU_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
BAIDU_CDN_HOST="cdn.baidubce.com"
_BAIDU_CDN_BCE_AUTH_RESULT=""
baidu_cdn_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if ! _baidu_cdn_load_credentials; then
return 1
fi
_getdeployconf DEPLOY_BAIDU_CDN_DOMAIN
if [ "$DEPLOY_BAIDU_CDN_DOMAIN" ]; then
_savedeployconf DEPLOY_BAIDU_CDN_DOMAIN "$DEPLOY_BAIDU_CDN_DOMAIN"
else
DEPLOY_BAIDU_CDN_DOMAIN="$_cdomain"
fi
# Build JSON "domains" array from space-separated domain list
_domains_json=""
for _d in $DEPLOY_BAIDU_CDN_DOMAIN; do
_d_e="$(_baidu_cdn_json_escape "$_d")"
if [ -z "$_domains_json" ]; then
_domains_json="\"${_d_e}\""
else
_domains_json="${_domains_json},\"${_d_e}\""
fi
done
# Build a valid cert name: must start with a letter, allow [A-Za-z0-9-/.], max 65 chars
_cert_name="$(printf "%s" "$_cdomain" | sed 's/\*\./wildcard./g;s/[^A-Za-z0-9./]/-/g' | cut -c 1-65)"
case "$_cert_name" in
[A-Za-z]*) ;;
*) _cert_name="c${_cert_name}" ;;
esac
# PEM content is already Base64 inside the -----BEGIN/END----- wrappers.
# The API expects the raw PEM as a JSON string, so newlines must be escaped as \n.
_cert_pem="$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')"
_key_pem="$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')"
_debug2 _cert_name "$_cert_name"
_debug2 _domains_json "[$_domains_json]"
# Build JSON payload
_payload="{\"domains\":[${_domains_json}],\"certificate\":{\"certName\":\"${_cert_name}\",\"certServerData\":\"${_cert_pem}\",\"certPrivateData\":\"${_key_pem}\"}}"
# Generate BCE v1 authorization header (query string included in canonical request)
_cdn_path="/v2/domain/certificate"
_cdn_query="action=put"
_ts="$(_utc_date | sed 's/ /T/')Z"
_content_type="application/json; charset=utf-8"
_payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)"
if ! _baidu_cdn_bce_auth "POST" "$_cdn_path" "$_cdn_query" "$BAIDU_CDN_HOST" "$_ts" "3600" "$_content_type" "$_payload_hash"; then
_err "Failed to sign request"
return 1
fi
_H1="Authorization: $_BAIDU_CDN_BCE_AUTH_RESULT"
_H2="x-bce-date: $_ts"
_H3="x-bce-content-sha256: $_payload_hash"
_H4="Host: $BAIDU_CDN_HOST"
_H5=""
_url="https://${BAIDU_CDN_HOST}${_cdn_path}?${_cdn_query}"
response="$(_post "$_payload" "$_url" "" "POST" "$_content_type")"
if [ "$?" != "0" ]; then
_err "Failed to call Baidu Cloud CDN API"
return 1
fi
_debug2 response "$response"
if _contains "$response" "\"certId\""; then
_info "Certificate deployed to Baidu Cloud CDN for: $DEPLOY_BAIDU_CDN_DOMAIN"
return 0
fi
_err "Failed to deploy certificate to Baidu Cloud CDN: $response"
return 1
}
# BCE v1 signing with canonical query string support.
# The CDN endpoint uses ?action=put so it must be included in the canonical request.
_baidu_cdn_bce_auth() {
_method="$1"
_uri="$2"
_query="$3"
_host="$4"
_ts="$5"
_expire="$6"
_ct="$7"
_payload_hash="$8"
_BAIDU_CDN_BCE_AUTH_RESULT=""
_auth_prefix="bce-auth-v1/${Baidu_AK}/${_ts}/${_expire}"
_signed_headers="content-type;host;x-bce-content-sha256;x-bce-date"
_canonical_uri="$(_baidu_cdn_bce_encode_path "$_uri")"
_host_e="$(printf "%s" "$_host" | _url_encode upper-hex)"
_date_e="$(printf "%s" "$_ts" | _url_encode upper-hex)"
_ct_e="$(printf "%s" "$_ct" | _url_encode upper-hex)"
_hash_e="$(printf "%s" "$_payload_hash" | _url_encode upper-hex)"
_canonical_headers="content-type:${_ct_e}
host:${_host_e}
x-bce-content-sha256:${_hash_e}
x-bce-date:${_date_e}"
_canonical_request="${_method}
${_canonical_uri}
${_query}
${_canonical_headers}"
_sk_hex="$(printf "%s" "$Baidu_SK" | _hex_dump | tr -d " ")"
_signing_key="$(_baidu_cdn_hmac_sha256_hexkey "$_sk_hex" "$_auth_prefix")"
_signing_key_hex="$(printf "%s" "$_signing_key" | _hex_dump | tr -d " ")"
_signature="$(_baidu_cdn_hmac_sha256_hexkey "$_signing_key_hex" "$_canonical_request")"
_BAIDU_CDN_BCE_AUTH_RESULT="${_auth_prefix}/${_signed_headers}/${_signature}"
}
_baidu_cdn_load_credentials() {
Baidu_AK="${Baidu_AK:-$(_readaccountconf_mutable Baidu_AK)}"
Baidu_SK="${Baidu_SK:-$(_readaccountconf_mutable Baidu_SK)}"
Baidu_AK="$(_baidu_cdn_trim_ws "$Baidu_AK")"
Baidu_SK="$(_baidu_cdn_trim_ws "$Baidu_SK")"
if [ -z "$Baidu_AK" ] || [ -z "$Baidu_SK" ]; then
_err "Baidu_AK and Baidu_SK are required"
return 1
fi
_saveaccountconf_mutable Baidu_AK "$Baidu_AK"
_saveaccountconf_mutable Baidu_SK "$Baidu_SK"
return 0
}
_baidu_cdn_bce_encode_path() {
_p="$1"
_out=""
if [ "${_p#"/"}" != "$_p" ]; then
_out="/"
fi
_rest="${_p#/}"
while [ -n "$_rest" ]; do
_seg="${_rest%%/*}"
if [ "$_seg" ]; then
if [ -z "$_out" ] || [ "$_out" = "/" ]; then
_out="${_out}$(printf "%s" "$_seg" | _url_encode upper-hex)"
else
_out="${_out}/$(printf "%s" "$_seg" | _url_encode upper-hex)"
fi
fi
if [ "${_rest#*/}" = "$_rest" ]; then
break
fi
_rest="${_rest#*/}"
done
if [ -z "$_out" ]; then
_out="/"
fi
printf "%s" "$_out"
}
_baidu_cdn_trim_ws() {
printf "%s" "$1" | tr '\r\n\t' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//'
}
_baidu_cdn_json_escape() {
_s="$1"
_s="$(printf "%s" "$_s" | tr -d '\r\n')"
printf "%s" "$_s" |
sed 's/\\/\\\\/g; s/ /\\t/g' |
_baidu_cdn_json_encode
}
_baidu_cdn_json_encode() {
_j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")"
printf "%s" "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
}
_baidu_cdn_hmac_sha256_hexkey() {
_key_hex="$1"
_msg="$2"
printf "%s" "$_msg" | _hmac sha256 "$_key_hex" hex
}

394
deploy/byteplus_alb.sh Normal file
View file

@ -0,0 +1,394 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034,SC2154
#
# acme.sh deploy hook: BytePlus Application Load Balancer (ALB)
# https://github.com/acmesh-official/acme.sh/wiki/deployhooks
#
# Deploys SSL/TLS certificates issued by acme.sh to BytePlus ALB.
# Supports automatic renewal with zero-downtime certificate rotation
# for certificates that have already been uploaded and have a saved
# BytePlus CertificateId.
#
# ┌─────────────────────────────────────────────────────────────────────┐
# │ FIRST TIME (new domain) │
# │ 1. acme.sh --issue -d example.com -w /var/www/html/ │
# │ 2. Upload/import the certificate to BytePlus ALB manually │
# │ 3. Save/configure the existing CertificateId for this hook │
# │ 4. Manually assign cert to ALB Listener (one-time only) │
# │ │
# │ RENEWAL (fully automatic after CertificateId is configured) │
# │ acme.sh cron triggers renew → deploy hook runs automatically │
# │ → ReplaceCertificate (UpdateMode=new) — single API call │
# │ → All attached listeners updated, old cert auto-deleted │
# └─────────────────────────────────────────────────────────────────────┘
#
# Required environment variables:
# export BYTEPLUS_ACCESS_KEY="AKAPxxxxxxxxxx"
# export BYTEPLUS_SECRET_KEY="your-secret-key"
#
# Optional environment variables:
# export BYTEPLUS_REGION="ap-southeast-3" # default: ap-southeast-3
# export BYTEPLUS_HOST="alb.ap-southeast-3.byteplusapi.com" # custom API host
# export BYTEPLUS_PROJECT_NAME="live" # default: "default" project
# export BYTEPLUS_CERT_NAME="" # default: acme-{domain}-{YYYYMMDD-HHMM}
# export BYTEPLUS_CERT_DESCRIPTION="" # default: empty
# export BYTEPLUS_DELETE_OLD_CERT="true" # default: true — auto-delete after replace
#
# API notes:
# - All BytePlus ALB APIs use GET with query string parameters
# - Request signing: HMAC-SHA256 with signed headers host;x-date
# - PublicKey/PrivateKey are URL-encoded (RFC 3986) in query string
# - ReplaceCertificate with UpdateMode=new uploads + replaces in 1 call
#
# Dependencies: curl, openssl, awk (standard on most Linux)
#
# Docs:
# Signing — https://docs.byteplus.com/en/docs/byteplus-platform/reference-how-to-calculate-a-signature
# ALB API — https://docs.byteplus.com/en/docs/byteplus-alb
# ══════════════════════════════════════════════════════════════════════════════
# Constants
# ══════════════════════════════════════════════════════════════════════════════
# SHA-256 hash of empty string (used for GET requests with no body)
_BYTEPLUS_EMPTY_HASH="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
# ══════════════════════════════════════════════════════════════════════════════
# Main deploy function — called by acme.sh
# ══════════════════════════════════════════════════════════════════════════════
byteplus_alb_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# ── 1. Load & validate credentials ──────────────────────────────────────────
# Preserve environment values before _getdeployconf (which may reset them)
_env_project_name="${BYTEPLUS_PROJECT_NAME:-}"
_env_delete_old="${BYTEPLUS_DELETE_OLD_CERT:-}"
_getdeployconf BYTEPLUS_ACCESS_KEY
_getdeployconf BYTEPLUS_SECRET_KEY
_getdeployconf BYTEPLUS_REGION
_getdeployconf BYTEPLUS_HOST
_getdeployconf BYTEPLUS_PROJECT_NAME
_getdeployconf BYTEPLUS_DELETE_OLD_CERT
_getdeployconf BYTEPLUS_CERT_NAME
# Restore from environment if _getdeployconf cleared them
if [ -z "$BYTEPLUS_PROJECT_NAME" ] && [ -n "$_env_project_name" ]; then
_debug "Restoring BYTEPLUS_PROJECT_NAME from environment"
BYTEPLUS_PROJECT_NAME="$_env_project_name"
fi
if [ -z "$BYTEPLUS_DELETE_OLD_CERT" ] && [ -n "$_env_delete_old" ]; then
BYTEPLUS_DELETE_OLD_CERT="$_env_delete_old"
fi
# Validate required credentials
if [ -z "$BYTEPLUS_ACCESS_KEY" ]; then
_err "BYTEPLUS_ACCESS_KEY is not set."
_err "Please run: export BYTEPLUS_ACCESS_KEY=\"your-access-key\""
return 1
fi
if [ -z "$BYTEPLUS_SECRET_KEY" ]; then
_err "BYTEPLUS_SECRET_KEY is not set."
_err "Please run: export BYTEPLUS_SECRET_KEY=\"your-secret-key\""
return 1
fi
# Save credentials for future runs
_savedeployconf BYTEPLUS_ACCESS_KEY "$BYTEPLUS_ACCESS_KEY"
_savedeployconf BYTEPLUS_SECRET_KEY "$BYTEPLUS_SECRET_KEY"
# Region (default: ap-southeast-3)
BYTEPLUS_REGION="${BYTEPLUS_REGION:-ap-southeast-3}"
_savedeployconf BYTEPLUS_REGION "$BYTEPLUS_REGION"
# Project name
if [ -n "$BYTEPLUS_PROJECT_NAME" ]; then
_savedeployconf BYTEPLUS_PROJECT_NAME "$BYTEPLUS_PROJECT_NAME"
_info "Using project: $BYTEPLUS_PROJECT_NAME"
else
_info "WARNING: BYTEPLUS_PROJECT_NAME is not set. Cert will go to 'default' project."
fi
# Delete old cert toggle (default: true)
BYTEPLUS_DELETE_OLD_CERT="${BYTEPLUS_DELETE_OLD_CERT:-true}"
_savedeployconf BYTEPLUS_DELETE_OLD_CERT "$BYTEPLUS_DELETE_OLD_CERT"
# API host — custom override or auto-build from region
if [ -n "$BYTEPLUS_HOST" ]; then
_BYTEPLUS_HOST="$BYTEPLUS_HOST"
_savedeployconf BYTEPLUS_HOST "$BYTEPLUS_HOST"
else
_BYTEPLUS_HOST="alb.${BYTEPLUS_REGION}.byteplusapi.com"
fi
_info "Using API host: $_BYTEPLUS_HOST"
_BYTEPLUS_SERVICE="alb"
# ── 2. Build certificate name ────────────────────────────────────────────────
_date_tag=$(date -u +%Y%m%d-%H%M)
# Replace wildcard * and dots for a valid cert name
_safe_domain=$(echo "$_cdomain" | sed 's/\*\.//g' | sed 's/\./-/g')
# Safe identifier version for deployconf keys: map all non [A-Za-z0-9_] to _
_conf_key=$(echo "$_cdomain" | sed 's/^\*\.//' | sed 's/[^A-Za-z0-9_]/_/g')
if [ -z "$BYTEPLUS_CERT_NAME" ]; then
BYTEPLUS_CERT_NAME="acme-${_safe_domain}-${_date_tag}"
fi
# Enforce BytePlus naming rules: start with letter, max 128 chars
BYTEPLUS_CERT_NAME=$(echo "$BYTEPLUS_CERT_NAME" | sed 's/[^A-Za-z0-9._-]/-/g')
case "$BYTEPLUS_CERT_NAME" in
[A-Za-z]*) ;;
*)
BYTEPLUS_CERT_NAME="a$BYTEPLUS_CERT_NAME"
;;
esac
BYTEPLUS_CERT_NAME=$(echo "$BYTEPLUS_CERT_NAME" | cut -c1-128)
_info "Certificate name: $BYTEPLUS_CERT_NAME"
# ── 3. Read cert and key ─────────────────────────────────────────────────────
# BytePlus requires NO blank lines between PEM blocks in the certificate chain
_public_key=$(_strip_blank_lines <"$_cfullchain" | tr -d '\r')
_private_key=$(_strip_blank_lines <"$_ckey" | tr -d '\r')
if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then
_err "Failed to read certificate or key file."
return 1
fi
# ── 4. Deploy: first-time upload or renewal replace ─────────────────────────
_getdeployconf "BYTEPLUS_CERT_ID_${_conf_key}"
_old_cert_id=$(eval echo "\$BYTEPLUS_CERT_ID_${_conf_key}")
if [ -z "$_old_cert_id" ]; then
_byteplus_first_time_deploy
else
_byteplus_renewal_deploy
fi
# Check if deploy step set _new_cert_id
if [ -z "$_new_cert_id" ]; then
return 1
fi
# ── 5. Save new CertificateId for next renewal ───────────────────────────────
_savedeployconf "BYTEPLUS_CERT_ID_${_conf_key}" "$_new_cert_id"
_info "Saved CertificateId '$_new_cert_id' for domain '$_cdomain'."
return 0
}
# ══════════════════════════════════════════════════════════════════════════════
# Deploy: First time — UploadCertificate
# ══════════════════════════════════════════════════════════════════════════════
_byteplus_first_time_deploy() {
_info "No previous CertificateId found."
_err "Refusing to upload certificate material because this hook passes PublicKey/PrivateKey as request parameters."
_err "Uploading a private key in the request URL can leak it via logs, proxies, and process listings."
_err "Please upload the certificate to BytePlus manually for the initial deployment, set BYTEPLUS_CERT_ID_${_conf_key} to that CertificateId, and rerun."
_err "This hook stores CertificateId values per domain using deployconf, so the variable name must include the current domain-specific suffix."
_err "This hook must be updated to send PublicKey and PrivateKey in a POST body before automatic first-time upload can be enabled safely."
return 1
}
# ══════════════════════════════════════════════════════════════════════════════
# Deploy: Renewal — ReplaceCertificate (UpdateMode=new)
# ══════════════════════════════════════════════════════════════════════════════
_byteplus_renewal_deploy() {
_info "Replacing old certificate '$_old_cert_id' (UpdateMode=new)..."
_err "Refusing to replace certificate material because this hook passes PublicKey/PrivateKey as request parameters."
_err "Uploading a private key in the request URL can leak it via logs, proxies, and process listings."
_err "Please replace the certificate in BytePlus manually for renewal until this hook is updated to send PublicKey and PrivateKey in a POST body safely."
return 1
}
# ══════════════════════════════════════════════════════════════════════════════
# Delete old certificate (with retry)
# ══════════════════════════════════════════════════════════════════════════════
_byteplus_delete_old_cert() {
_del_cert_id="$1"
_info "Waiting 5s for cert status to settle..."
_sleep 5
_info "Deleting old certificate '$_del_cert_id'..."
_del_response=$(_byteplus_alb_api "DeleteCertificate" "CertificateId=${_del_cert_id}")
if echo "$_del_response" | grep -q '"Error"'; then
_info "Delete failed, retrying in 10s..."
_sleep 10
_del_response=$(_byteplus_alb_api "DeleteCertificate" "CertificateId=${_del_cert_id}")
if echo "$_del_response" | grep -q '"Error"'; then
_info "Warning: Could not delete old certificate '$_del_cert_id'."
_info "Error: $(_byteplus_extract_error "$_del_response")"
_info "Please remove it manually from BytePlus Console."
else
_info "Old certificate '$_del_cert_id' deleted (retry succeeded)."
fi
else
_info "Old certificate '$_del_cert_id' deleted."
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# JSON response helpers
# ══════════════════════════════════════════════════════════════════════════════
# Extract CertificateId from API response JSON
_byteplus_extract_cert_id() {
echo "$1" | _egrep_o '"CertificateId"\s*:\s*"[^"]*"' | head -1 | _egrep_o '"[^"]*"$' | tr -d '"'
}
# Extract error message from API response JSON
_byteplus_extract_error() {
_code=$(echo "$1" | _egrep_o '"Code"\s*:\s*"[^"]*"' | head -1 | _egrep_o '"[^"]*"$' | tr -d '"')
_msg=$(echo "$1" | _egrep_o '"Message"\s*:\s*"[^"]*"' | head -1 | _egrep_o '"[^"]*"$' | tr -d '"')
if [ -n "$_code" ]; then
printf '%s — %s' "$_code" "$_msg"
else
printf '%s' "$1"
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# BytePlus ALB API caller
# ══════════════════════════════════════════════════════════════════════════════
# Usage: _byteplus_alb_api ACTION [param1=val1] [param2=val2] ...
# All parameters sent via GET query string. Signing: HMAC-SHA256, host;x-date.
_byteplus_alb_api() {
_action="$1"
shift
# Build query string — all params go in URL
_query_params="Action=${_action}&Version=2020-04-01"
for _param in "$@"; do
_pname="${_param%%=*}"
_pval="${_param#*=}"
_query_params="${_query_params}&${_pname}=$(_byteplus_urlencode "$_pval")"
done
# Timestamps
_x_date=$(date -u +%Y%m%dT%H%M%SZ)
_date_only=$(date -u +%Y%m%d)
# Sort query params for canonical request
_sorted_query=$(echo "$_query_params" | tr '&' '\n' | LC_ALL=C sort | tr '\n' '&' | sed 's/&$//')
# Canonical headers — only host and x-date
_canonical_headers="host:${_BYTEPLUS_HOST}
x-date:${_x_date}
"
_signed_headers="host;x-date"
# Canonical request
_canonical_request="GET
/
${_sorted_query}
${_canonical_headers}
${_signed_headers}
${_BYTEPLUS_EMPTY_HASH}"
# Do not log _canonical_request because the query string may contain
# URL-encoded certificate or private key material.
# Hash of canonical request
# _digest is provided by acme.sh and works across OpenSSL versions.
_cr_hash=$(printf '%s' "$_canonical_request" | _digest sha256 hex)
# Credential scope
_credential_scope="${_date_only}/${BYTEPLUS_REGION}/${_BYTEPLUS_SERVICE}/request"
# String to sign
_string_to_sign="HMAC-SHA256
${_x_date}
${_credential_scope}
${_cr_hash}"
_debug2 _string_to_sign "$_string_to_sign"
# Signing key derivation (HMAC chain)
# _hmac <algo> <hex-key> reads data from stdin and returns a hex digest.
# acme.sh's _hmac abstracts away OpenSSL version differences, so this works
# on both modern (-mac HMAC -macopt hexkey:) and older (-hmac) OpenSSL builds.
#
# The first step seeds the chain from the raw secret key, so we convert it
# to hex first with _hex_dump (also an acme.sh built-in).
_secret_hex=$(printf '%s' "$BYTEPLUS_SECRET_KEY" | _hex_dump | tr -d ' \n')
_k_date=$(printf '%s' "$_date_only" | _hmac sha256 "$_secret_hex" hex)
_k_region=$(printf '%s' "$BYTEPLUS_REGION" | _hmac sha256 "$_k_date" hex)
_k_service=$(printf '%s' "$_BYTEPLUS_SERVICE" | _hmac sha256 "$_k_region" hex)
_k_signing=$(printf '%s' "request" | _hmac sha256 "$_k_service" hex)
# Final signature
_signature=$(printf '%s' "$_string_to_sign" | _hmac sha256 "$_k_signing" hex)
# Authorization header
_auth="HMAC-SHA256 Credential=${BYTEPLUS_ACCESS_KEY}/${_credential_scope}, SignedHeaders=${_signed_headers}, Signature=${_signature}"
_secure_debug2 _auth "$_auth"
# Send request parameters in the POST body instead of the URL query string.
# This avoids exposing sensitive or large values in debug-logged URLs and
# reduces the risk of exceeding URL length limits.
_url="https://${_BYTEPLUS_HOST}/"
_body="$_sorted_query"
_saved_H1="${_H1:-}"
_saved_H2="${_H2:-}"
_saved_H3="${_H3:-}"
_saved_H4="${_H4:-}"
_saved_H5="${_H5:-}"
_H1="Authorization: ${_auth}"
_H2="X-Date: ${_x_date}"
_H3="Host: ${_BYTEPLUS_HOST}"
_H4="Content-Type: application/x-www-form-urlencoded"
_H5=""
_response="$(_post "$_body" "$_url" "" "POST")"
_request_ret="$?"
_H1="$_saved_H1"
_H2="$_saved_H2"
_H3="$_saved_H3"
_H4="$_saved_H4"
_H5="$_saved_H5"
if [ "$_request_ret" != "0" ]; then
_err "byteplus_alb_api request failed for [$_action]"
return 1
fi
_debug2 "_byteplus_alb_api response [$_action]" "$_response"
printf '%s' "$_response"
}
# ══════════════════════════════════════════════════════════════════════════════
# URL encode (RFC 3986)
# ══════════════════════════════════════════════════════════════════════════════
_byteplus_urlencode() {
printf '%s' "$1" | _url_encode
}

View file

@ -52,7 +52,15 @@ cpanel_uapi_deploy() {
# read cert and key files and urlencode both
_cert=$(_url_encode <"$_ccert")
_key=$(_url_encode <"$_ckey")
# with --signcsr the private key was never handed to acme.sh, so the key
# file does not exist; skip it instead of spilling a shell redirection
# error on every renewal (cPanel keeps using the already-installed key)
if [ -f "$_ckey" ]; then
_key=$(_url_encode <"$_ckey")
else
_debug "Key file $_ckey does not exist (csr mode), not sending a key."
_key=""
fi
_debug2 _cert "$_cert"
_debug2 _key "$_key"
@ -79,7 +87,11 @@ cpanel_uapi_deploy() {
# Auto mode
if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then
# call API for site config
_response=$(uapi DomainInfo list_domains)
if [ -n "$_uapi_user" ]; then
_response=$(uapi --user="$_uapi_user" DomainInfo list_domains)
else
_response=$(uapi DomainInfo list_domains)
fi
# exit if error in response
if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
_err "Error in deploying certificate - cannot retrieve sitelist:"
@ -194,7 +206,8 @@ __cpanel_parse_response() {
printf("%s%s=%s\n", prefix, $2, $3);
}
}' |
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p'
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p' |
sed -e 's/^"//' -e 's/"$//' # YAML double-quotes values starting with '*' (wildcard subdomains)
}
# Load parameter by prefix+name - fallback to default if not set, and save to config

View file

@ -3,6 +3,8 @@
#DEPLOY_DOCKER_CONTAINER_LABEL="xxxxxxx"
#DEPLOY_DOCKER_CONTAINER_KEY_FILE="/path/to/key.pem"
#DEPLOY_DOCKER_CONTAINER_KEY_MODE="0640"
#DEPLOY_DOCKER_CONTAINER_KEY_OWNER="1000:1000"
#DEPLOY_DOCKER_CONTAINER_CERT_FILE="/path/to/cert.pem"
#DEPLOY_DOCKER_CONTAINER_CA_FILE="/path/to/ca.pem"
#DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/path/to/fullchain.pem"
@ -71,6 +73,18 @@ docker_deploy() {
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_FILE "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_CERT_FILE
_debug2 DEPLOY_DOCKER_CONTAINER_CERT_FILE "$DEPLOY_DOCKER_CONTAINER_CERT_FILE"
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
@ -112,6 +126,20 @@ docker_deploy() {
if ! _docker_cp "$_cid" "$_ckey" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
return 1
fi
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
_info "Setting key file owner to $DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
if ! _docker_exec "$_cid" chown "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
_err "Can not change owner of key file in container"
return 1
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
_info "Setting key file mode to $DEPLOY_DOCKER_CONTAINER_KEY_MODE"
if ! _docker_exec "$_cid" chmod "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
_err "Can not change mode of key file in container"
return 1
fi
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
@ -189,10 +217,22 @@ _docker_exec() {
_debug2 cjson "$cjson"
execid="$(echo "$cjson" | cut -d '"' -f 4)"
_debug execid "$execid"
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": false,\"Tty\": false}")"
#Detach:true is required for podman's docker-compatible API: with
#Detach:false it streams the command output on the connection, so the
#non-empty response was misread as an error (issue #4977). The real
#result is checked via the exec inspect ExitCode below instead.
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": true,\"Tty\": false}")"
_debug2 ejson "$ejson"
if [ "$ejson" ]; then
_err "$ejson"
_et=0
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
while _contains "$ijson" "\"Running\":true" && [ "$_et" -lt 10 ]; do
sleep 1
_et="$(_math "$_et" + 1)"
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
done
_debug2 ijson "$ijson"
if ! echo "$ijson" | _egrep_o "\"ExitCode\": *0[,}]" >/dev/null 2>&1; then
_err "docker exec error: $ijson"
return 1
fi
else

175
deploy/fortigate.sh Normal file
View file

@ -0,0 +1,175 @@
#!/usr/bin/env sh
# Script to deploy a certificate to FortiGate via API and set it as the current web GUI certificate.
#
# FortiGate's native ACME integration does not support wildcard certificates or domain validation,
# and is not supported if you have a custom management web port (eg. DNAT web traffic).
#
# REQUIRED:
# export FGT_HOST="fortigate_hostname-or-ip"
# export FGT_TOKEN="fortigate_api_token"
#
# OPTIONAL:
# export FGT_PORT="10443" # Custom HTTPS port (defaults to 443 if not set)
#
# Run `acme.sh --deploy -d example.com --deploy-hook fortigate --insecure` to use this script.
# `--insecure` is required on first run if not already using a valid SSL certificate on firewall.
# Function to parse a FortiGate API response
_fortigate_parse_response() {
_fortigate_response="$1"
_fortigate_func="$2"
_fortigate_status=$(echo "$_fortigate_response" | _egrep_o '"status":[ ]*"[^"]*"' | cut -d '"' -f 4)
if [ "$_fortigate_status" != "success" ]; then
_err "[$_fortigate_func] Operation failed. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot."
return 1
fi
_debug "[$_fortigate_func] Operation successful."
return 0
}
# Function to deploy a base64-encoded certificate to the firewall
_fortigate_deployer() {
_fortigate_cert_base64=$(_base64 <"$_fortigate_cfullchain" | tr -d '\n')
_fortigate_key_base64=$(_base64 <"$_fortigate_ckey" | tr -d '\n')
_fortigate_payload=$(
cat <<EOF
{
"type": "regular",
"scope": "global",
"certname": "$_fortigate_cert_name",
"key_file_content": "$_fortigate_key_base64",
"file_content": "$_fortigate_cert_base64"
}
EOF
)
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/local/import"
_debug "Uploading certificate via URL: $_fortigate_url"
_H1="Authorization: Bearer $FGT_TOKEN"
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "POST" "application/json")
_debug "FortiGate API Response: $_fortigate_response"
_fortigate_parse_response "$_fortigate_response" "Deploying certificate" || return 1
}
# Function to upload a CA certificate to the firewall
# FortiGate does not automatically extract the CA from the full chain.
_fortigate_upload_ca_cert() {
_fortigate_ca_base64=$(_base64 <"$_fortigate_cca" | tr -d '\n')
_fortigate_payload=$(
cat <<EOF
{
"import_method": "file",
"scope": "global",
"file_content": "$_fortigate_ca_base64"
}
EOF
)
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/ca/import"
_debug "Uploading CA certificate via URL: $_fortigate_url"
_H1="Authorization: Bearer $FGT_TOKEN"
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "POST" "application/json")
_debug "FortiGate API CA Response: $_fortigate_response"
# FortiGate error -328 means that the CA certificate already exists.
if echo "$_fortigate_response" | grep -q '"error":[ ]*-328'; then
_debug "CA certificate already exists. Skipping CA upload."
return 0
fi
_fortigate_parse_response "$_fortigate_response" "Deploying CA certificate" || return 1
}
# Function to activate the new certificate
_fortigate_set_active_web_cert() {
_fortigate_payload=$(
cat <<EOF
{
"admin-server-cert": "$_fortigate_cert_name"
}
EOF
)
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/system/global"
_debug "Setting GUI certificate..."
_H1="Authorization: Bearer $FGT_TOKEN"
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "PUT" "application/json")
_fortigate_parse_response "$_fortigate_response" "Assigning active certificate" || return 1
}
# Function to clean up the previously deployed certificate
_fortigate_cleanup_previous_certificate() {
_getdeployconf FGT_LAST_CERT
if [ -n "$FGT_LAST_CERT" ] && [ "$FGT_LAST_CERT" != "$_fortigate_cert_name" ]; then
_debug "Found previously deployed certificate: $FGT_LAST_CERT. Deleting it."
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/vpn.certificate/local/${FGT_LAST_CERT}"
_H1="Authorization: Bearer $FGT_TOKEN"
_fortigate_response=$(_post "" "$_fortigate_url" "" "DELETE" "application/json")
_debug "Delete certificate API response: $_fortigate_response"
_fortigate_parse_response "$_fortigate_response" "Delete previous certificate" || return 1
else
_debug "No previous certificate found."
fi
}
# Main deploy-hook function
fortigate_deploy() {
# Include date and time to ensure unique names.
_fortigate_cert_name="$(echo "$1" | sed 's/*/WILDCARD_/g')_$(date -u +"%Y-%m-%d_%H-%M-%S")"
_fortigate_ckey="$2"
_fortigate_cca="$4"
_fortigate_cfullchain="$5"
if [ ! -f "$_fortigate_ckey" ] || [ ! -f "$_fortigate_cfullchain" ]; then
_err "Valid key and/or certificate not found."
return 1
fi
# Save required environment variables if set; otherwise load saved values.
for _fortigate_var in FGT_HOST FGT_TOKEN FGT_PORT; do
if [ -n "$(eval echo "\$$_fortigate_var")" ]; then
_debug "Detected ENV variable $_fortigate_var. Saving to file."
_savedeployconf "$_fortigate_var" "$(eval echo "\$$_fortigate_var")" 1
else
_debug "Attempting to load variable $_fortigate_var from file."
_getdeployconf "$_fortigate_var"
fi
done
if [ -z "$FGT_HOST" ] || [ -z "$FGT_TOKEN" ]; then
_err "FGT_HOST and FGT_TOKEN must be set."
return 1
fi
FGT_PORT="${FGT_PORT:-443}"
_debug "Using FortiGate port: $FGT_PORT"
# Upload the new certificate.
_fortigate_deployer || return 1
# Upload the CA certificate.
if [ -n "$_fortigate_cca" ] && [ -f "$_fortigate_cca" ]; then
_fortigate_upload_ca_cert || return 1
else
_debug "No CA certificate provided."
fi
# Activate the new certificate.
_fortigate_set_active_web_cert || return 1
# Delete the previously deployed certificate only after successful activation.
_fortigate_cleanup_previous_certificate || return 1
# Save the new certificate name for cleanup during the next deployment.
_savedeployconf "FGT_LAST_CERT" "$_fortigate_cert_name" 1
}

View file

@ -57,7 +57,7 @@ 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.gcore.com/auth/jwt/login")
_response=$(_post "$_request" "https://api.gcore.com/iam/auth/jwt/login")
_debug _response "$_response"
_regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$"
_debug _regex "$_regex"

View file

@ -43,7 +43,8 @@
# needing to reload HAProxy. Default is "no".
#
# Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat
# address format.
# address format. The certificate can be deployed to a comma separated ',' list
# of hosts ("TCP4:10.0.0.1:1999,TCP4:10.0.0.2:1999")
#
# export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock"
#
@ -193,7 +194,6 @@ haproxy_deploy() {
_issuer="${_pem}.issuer"
_ocsp="${_pem}.ocsp"
_reload="${Le_Deploy_haproxy_reload}"
_statssock="${Le_Deploy_haproxy_stats_socket}"
_info "Deploying PEM file"
# Create a temporary PEM file
@ -272,12 +272,18 @@ haproxy_deploy() {
_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 1.1+ expects -header Host=value (one argument), while
# LibreSSL keeps the old two-argument form -header Host value at any
# version (3.x/4.x), so it must be detected by name, not by number.
_openssl_name=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f1)
_openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
_debug _openssl_name "${_openssl_name}"
_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
if [ "${_openssl_name}" = "LibreSSL" ]; then
_header_sep=" "
elif [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
_header_sep="="
else
_header_sep=" "
@ -327,62 +333,67 @@ haproxy_deploy() {
# Update certificate over HAProxy stats socket or master CLI.
if _exists socat; then
# look for the certificate on the stats socket, to chose between updating or creating one
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
_debug _socat_cert_cmd "${_socat_cert_cmd}"
eval "${_socat_cert_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_newcert="1"
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
eval "${_socat_crtlist_show_cmd}"
IFS=','
for _statssock in ${Le_Deploy_haproxy_stats_socket}; do
# look for the certificate on the stats socket, to choose between updating or creating one
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
_debug _socat_cert_cmd "${_socat_cert_cmd}"
eval "${_socat_cert_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
return "${_ret}"
_newcert="1"
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
eval "${_socat_crtlist_show_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
return "${_ret}"
fi
# create a new certificate
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
_debug _socat_new_cmd "${_socat_new_cmd}"
eval "${_socat_new_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Couldn't create '${_pem}' in haproxy"
return "${_ret}"
fi
else
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
fi
# create a new certificate
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
_debug _socat_new_cmd "${_socat_new_cmd}"
eval "${_socat_new_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Couldn't create '${_pem}' in haproxy"
return "${_ret}"
fi
else
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
fi
_socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'"
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
eval "${_socat_cert_set_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't update '${_pem}' in haproxy"
return "${_ret}"
fi
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
eval "${_socat_cert_commit_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't commit '${_pem}' in haproxy"
return ${_ret}
fi
if [ "${_newcert}" = "1" ]; then
# if this is a new certificate, it needs to be inserted into the crt-list`
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
eval "${_socat_cert_add_cmd}"
# printf %b, not "echo -e": dash's echo has no -e and sends a literal "-e " to the socket.
# "Transaction updated" is replied instead of "created" when an uncommitted transaction exists.
_socat_cert_set_cmd="printf '%b\n' '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -qE 'Transaction (created|updated)'"
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
eval "${_socat_cert_set_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't update '${_pem}' in haproxy"
return "${_ret}"
fi
fi
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
eval "${_socat_cert_commit_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't commit '${_pem}' in haproxy"
return ${_ret}
fi
if [ "${_newcert}" = "1" ]; then
# if this is a new certificate, it needs to be inserted into the crt-list`
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
eval "${_socat_cert_add_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't update '${_pem}' in haproxy"
return "${_ret}"
fi
fi
done
else
_err "'socat' is not available, couldn't update over ${_socketname}"
fi

114
deploy/ikuai.sh Normal file
View file

@ -0,0 +1,114 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to ikuai using curl
#
# it requires following environment variables:
#
# IKUAI_SCHEME="http" - http or https , defaults to "http"
# IKUAI_HOSTNAME="localhost" - host , defaults to "192.168.9.1"
# IKUAI_PORT="80" - port , defaults to "80"
# IKUAI_USERNAME="admin" - username , defaults to "admin"
# IKUAI_PASSWORD="yourPassword" - password
# IKUAI_CERT_ID=1 - ikuai cert id , defaults to 1, and only 1 is supported for now !!!
#
#returns 0 means success, otherwise error.
#
######## Public functions #####################
#
#domain keyfile certfile cafile fullchain
ikuai_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# Get deploy conf
_getdeployconf IKUAI_SCHEME
_getdeployconf IKUAI_HOSTNAME
_getdeployconf IKUAI_PORT
_getdeployconf IKUAI_USERNAME
_getdeployconf IKUAI_PASSWORD
_getdeployconf IKUAI_CERT_ID
# Use default if not provided
[ -n "$IKUAI_SCHEME" ] || IKUAI_SCHEME="http"
[ -n "$IKUAI_HOSTNAME" ] || IKUAI_HOSTNAME="192.168.9.1"
[ -n "$IKUAI_PORT" ] || IKUAI_PORT=80
[ -n "$IKUAI_USERNAME" ] || IKUAI_USERNAME="admin"
[ -n "$IKUAI_CERT_ID" ] || IKUAI_CERT_ID=1
if [ -z "$IKUAI_PASSWORD" ]; then
_err "please define IKUAI_PASSWORD."
return 1
fi
_debug2 IKUAI_SCHEME "$IKUAI_SCHEME"
_debug2 IKUAI_HOSTNAME "$IKUAI_HOSTNAME"
_debug2 IKUAI_PORT "$IKUAI_PORT"
_debug2 IKUAI_USERNAME "$IKUAI_USERNAME"
_secure_debug2 IKUAI_PASSWORD "$IKUAI_PASSWORD"
_info "Login to ikuai ..."
_ikuai_url="$IKUAI_SCHEME://$IKUAI_HOSTNAME:$IKUAI_PORT"
_pass_md5="$(printf "%s" "$IKUAI_PASSWORD" | _digest md5 hex | _lower_case)"
_pass_salt="$(printf "salt_11%s" "$IKUAI_PASSWORD" | _base64)"
_debug2 _ikuai_url "$_ikuai_url"
_login_req="{\"username\":\"$IKUAI_USERNAME\",\"passwd\":\"$_pass_md5\",\"pass\":\"$_pass_salt\",\"remember_password\":\"\"}"
_response=$(_post "$_login_req" "$_ikuai_url/Action/login" "" "POST" "application/json")
_err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)"
# check ErrMsg
if [ "$_err_msg" != "Success" ]; then
_err "Failed to login to ikuai: $_err_msg"
return 1
fi
# check cookie
_cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | sed 's/;.*//')"
if [ -z "$_cookie" ]; then
_err "Fail to get the cookie."
return 1
fi
# Set cookie header
_H1="Cookie: $_cookie; username=$IKUAI_USERNAME; login=1"
_info "Deploy the cert to ikuai ... "
# Should replace \n to @ ," " to #
_cert_content_single_line="$(tr <"$_cfullchain" '\n' '@' | tr ' ' '#')"
_key_content_single_line="$(tr <"$_ckey" '\n' '@' | tr ' ' '#')"
_debug2 _cert_content_single_line "$_cert_content_single_line"
_secure_debug2 _key_content_single_line "$_key_content_single_line"
_key_manager_req="{\"func_name\":\"key_manager\",\"action\":\"save\",\"param\":{\"ca\":\"$_cert_content_single_line\",\"key\":\"$_key_content_single_line\",\"id\":$IKUAI_CERT_ID,\"enabled\":\"yes\",\"comment\":\"\"}}"
_response=$(_post "$_key_manager_req" "$_ikuai_url/Action/call" "" "POST" "application/json")
_err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)"
# check ErrMsg
if [ "$_err_msg" != "Success" ]; then
_err "Failed to deploy the cert to ikuai: $_err_msg"
return 1
fi
_info "Save the deploy config ... "
# Save the config
_savedeployconf IKUAI_SCHEME "$IKUAI_SCHEME"
_savedeployconf IKUAI_HOSTNAME "$IKUAI_HOSTNAME"
_savedeployconf IKUAI_PORT "$IKUAI_PORT"
_savedeployconf IKUAI_USERNAME "$IKUAI_USERNAME"
_savedeployconf IKUAI_PASSWORD "$IKUAI_PASSWORD"
_savedeployconf IKUAI_CERT_ID "$IKUAI_CERT_ID"
_info "Successfully deployed certificate to ikuai. Enjoy! :>"
return 0
}

View file

@ -56,7 +56,7 @@ kemplm_deploy() {
_info "Check if certificate is already present"
_list_request="{\"cmd\": \"listcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\"}"
_debug3 _list_request "${_list_request}"
_kemp_cert_count=$(HTTPS_INSECURE=1 _post "${_list_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "${_kemp_domain}")
_kemp_cert_count=$(HTTPS_INSECURE=1 _post "${_list_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "^${_kemp_domain}$")
_debug2 _kemp_cert_count "${_kemp_cert_count}"
_kemp_replace_cert=1
@ -86,6 +86,7 @@ kemplm_deploy() {
_info "Upload successful"
else
_err "Upload failed: ${_kemp_post_message}"
_retval=1
fi
else
_err "Upload failed"

View file

@ -83,7 +83,7 @@ keyhelp_deploy() {
_request_body="submit=1&certificate_name=$certificate_name&add_type=upload&text_private_key=$encoded_key&text_certificate=$encoded_ccert&text_ca_certificate=$encoded_cca"
_H1="Cookie: $_cookie"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST")
_message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_message=$(echo "$_response" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_info "_message" "$_message"
if [ -z "$_message" ]; then
_err "Fail to upload certificate."
@ -118,7 +118,7 @@ keyhelp_deploy() {
_request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&path=$path&is_prefer_https=$is_prefer_https&hsts_enabled=$hsts_enabled&certificate_type=custom&certificate_id=$cert_value&enforce_https=$DEPLOY_KEYHELP_ENFORCE_HTTPS"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit" "" "POST")
_message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_message=$(echo "$_response" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_info "_message" "$_message"
if [ -z "$_message" ]; then
_err "Fail to apply certificate."

157
deploy/localcopy.sh Normal file
View file

@ -0,0 +1,157 @@
#!/usr/bin/env sh
# Deploy-hook to very simply copy files to set directories and then
# execute whatever reloadcmd the admin needs afterwards. This can be
# useful for configurations where the "multideploy" hook (in development)
# is used or when an admin wants ACME.SH to renew certs but needs to
# manually configure deployment via an external script
# (e.g. The deploy-freenas script for TrueNAS Core/Scale
# https://github.com/danb35/deploy-freenas/ )
#
# If the same file is configured for the certificate key
# and the certificate and/or full chain, a combined PEM file will
# be output instead.
#
# Environment variables to be utilized are as follows:
#
# DEPLOY_LOCALCOPY_CERTKEY - /path/to/target/cert.key
# DEPLOY_LOCALCOPY_CERTIFICATE - /path/to/target/cert.cer
# DEPLOY_LOCALCOPY_FULLCHAIN - /path/to/target/fullchain.cer
# DEPLOY_LOCALCOPY_CA - /path/to/target/ca.cer
# DEPLOY_LOCALCOPY_PFX - /path/to/target/cert.pfx
# DEPLOY_LOCALCOPY_RELOADCMD - "echo 'this is my cmd'"
######## Public functions #####################
#domain keyfile certfile cafile fullchain
localcopy_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cpfx="$6"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
_getdeployconf DEPLOY_LOCALCOPY_CERTIFICATE
_getdeployconf DEPLOY_LOCALCOPY_CERTKEY
_getdeployconf DEPLOY_LOCALCOPY_FULLCHAIN
_getdeployconf DEPLOY_LOCALCOPY_CA
_getdeployconf DEPLOY_LOCALCOPY_RELOADCMD
_getdeployconf DEPLOY_LOCALCOPY_PFX
_combined_target=""
_combined_srccert=""
# Create PEM file
if [ "$DEPLOY_LOCALCOPY_CERTKEY" ] &&
{ [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_FULLCHAIN" ] ||
[ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; }; then
_combined_target="$DEPLOY_LOCALCOPY_CERTKEY"
_savedeployconf DEPLOY_LOCALCOPY_CERTKEY "$DEPLOY_LOCALCOPY_CERTKEY"
if [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then
_combined_srccert="$_ccert"
_savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE"
DEPLOY_LOCALCOPY_CERTIFICATE=""
fi
if [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then
_combined_srccert="$_cfullchain"
_savedeployconf DEPLOY_LOCALCOPY_FULLCHAIN "$DEPLOY_LOCALCOPY_FULLCHAIN"
DEPLOY_LOCALCOPY_FULLCHAIN=""
fi
DEPLOY_LOCALCOPY_CERTKEY=""
_info "Creating combined PEM"
_debug "Creating combined PEM at $_combined_target"
if ! [ -f "$_combined_target" ]; then
touch "$_combined_target" || return 1
chmod 600 "$_combined_target"
fi
if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then
_err "Failed to create PEM file"
return 1
fi
fi
if [ "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then
_info "Copying certificate"
_debug "Copying $_ccert to $DEPLOY_LOCALCOPY_CERTIFICATE"
if ! cat "$_ccert" >"$DEPLOY_LOCALCOPY_CERTIFICATE"; then
_err "Failed to copy certificate, aborting."
return 1
fi
_savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE"
fi
if [ "$DEPLOY_LOCALCOPY_CERTKEY" ]; then
_info "Copying certificate key"
_debug "Copying $_ckey to $DEPLOY_LOCALCOPY_CERTKEY"
if ! [ -f "$DEPLOY_LOCALCOPY_CERTKEY" ]; then
touch "$DEPLOY_LOCALCOPY_CERTKEY" || return 1
chmod 600 "$DEPLOY_LOCALCOPY_CERTKEY"
fi
if ! cat "$_ckey" >"$DEPLOY_LOCALCOPY_CERTKEY"; then
_err "Failed to copy certificate key, aborting."
return 1
fi
_savedeployconf DEPLOY_LOCALCOPY_CERTKEY "$DEPLOY_LOCALCOPY_CERTKEY"
fi
if [ "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then
_info "Copying fullchain"
_debug "Copying $_cfullchain to $DEPLOY_LOCALCOPY_FULLCHAIN"
if ! cat "$_cfullchain" >"$DEPLOY_LOCALCOPY_FULLCHAIN"; then
_err "Failed to copy fullchain, aborting."
return 1
fi
_savedeployconf DEPLOY_LOCALCOPY_FULLCHAIN "$DEPLOY_LOCALCOPY_FULLCHAIN"
fi
if [ "$DEPLOY_LOCALCOPY_CA" ]; then
_info "Copying CA"
_debug "Copying $_cca to $DEPLOY_LOCALCOPY_CA"
if ! cat "$_cca" >"$DEPLOY_LOCALCOPY_CA"; then
_err "Failed to copy CA, aborting."
return 1
fi
_savedeployconf DEPLOY_LOCALCOPY_CA "$DEPLOY_LOCALCOPY_CA"
fi
if [ "$DEPLOY_LOCALCOPY_PFX" ]; then
_info "Copying PFX"
_debug "Copying $_cpfx to $DEPLOY_LOCALCOPY_PFX"
if ! [ -f "$DEPLOY_LOCALCOPY_PFX" ]; then
touch "$DEPLOY_LOCALCOPY_PFX" || return 1
chmod 600 "$DEPLOY_LOCALCOPY_PFX"
fi
if ! cat "$_cpfx" >"$DEPLOY_LOCALCOPY_PFX"; then
_err "Failed to copy PFX, aborting."
return 1
fi
_savedeployconf DEPLOY_LOCALCOPY_PFX "$DEPLOY_LOCALCOPY_PFX"
fi
_reload=$DEPLOY_LOCALCOPY_RELOADCMD
_debug "Running reloadcmd $_reload"
if [ -z "$_reload" ]; then
_info "Reloadcmd not provided, skipping."
else
_info "Reloading"
if eval "$_reload"; then
_info "Reload successful."
_savedeployconf DEPLOY_LOCALCOPY_RELOADCMD "$DEPLOY_LOCALCOPY_RELOADCMD" "base64"
else
_err "Reload failed."
return 1
fi
fi
_info "$(__green "'localcopy' deploy success")"
return 0
}

287
deploy/multideploy.sh Normal file
View file

@ -0,0 +1,287 @@
#!/usr/bin/env sh
################################################################################
# ACME.sh 3rd party deploy plugin for multiple (same) services
################################################################################
# Authors: tomo2403 (creator), https://github.com/tomo2403
# Updated: 2025-03-01
# Issues: https://github.com/acmesh-official/acme.sh/issues and mention @tomo2403
################################################################################
# Usage (shown values are the examples):
# 1. Set optional environment variables
# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set"
# A name without a leading '/' is looked up in the certificate directory
# of the domain. An absolute path is used as is, so a single deploy file
# can be shared by all domains, e.g.
# - export MULTIDEPLOY_FILENAME="/etc/acme/multideploy.yml"
#
# 2. Run command:
# acme.sh --deploy --deploy-hook multideploy -d example.com
################################################################################
# Dependencies:
# - yq
################################################################################
# Return value:
# 0 means success, otherwise error.
################################################################################
MULTIDEPLOY_VERSION="1.0"
# Description: This function handles the deployment of certificates to multiple services.
# It processes the provided certificate files and deploys them according to the
# configuration specified in the multideploy file.
#
# Parameters:
# _cdomain - The domain name for which the certificate is issued.
# _ckey - The private key file for the certificate.
# _ccert - The certificate file.
# _cca - The CA (Certificate Authority) file.
# _cfullchain - The full chain certificate file.
# _cpfx - The PFX (Personal Information Exchange) file.
multideploy_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cpfx="$6"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
_getdeployconf MULTIDEPLOY_FILENAME
if [ -z "$MULTIDEPLOY_FILENAME" ]; then
MULTIDEPLOY_FILENAME="multideploy.yml"
_info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'."
else
_savedeployconf "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME"
_debug2 "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME"
fi
if ! file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME"); then
_err "Failed to preprocess deploy file."
return 1
fi
_debug3 "File" "$file"
# Deploy to services
_deploy_services "$file"
_exitCode="$?"
return "$_exitCode"
}
# Description:
# This function preprocesses the deploy file by checking if 'yq' is installed,
# verifying the existence of the deploy file, and ensuring only one deploy file is present.
# Arguments:
# $@ - Posible deploy file names. A name starting with '/' is treated as an
# absolute path, any other name is relative to the domain directory.
# Usage:
# _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?"
_preprocess_deployfile() {
# Check if yq is installed
if ! command -v yq >/dev/null 2>&1; then
_err "yq is not installed! Please install yq and try again."
return 1
fi
_debug3 "yq is installed."
# Check if deploy file exists
found_file=""
for file in "$@"; do
if _startswith "$file" "/"; then
_multideploy_path="$file"
else
_multideploy_path="$DOMAIN_PATH/$file"
fi
_debug3 "Checking file" "$_multideploy_path"
if [ -f "$_multideploy_path" ]; then
_debug3 "File found"
if [ -n "$found_file" ]; then
_err "Multiple deploy files found. Please keep only one deploy file."
return 1
fi
found_file="$_multideploy_path"
else
_debug3 "File not found"
fi
done
if [ -z "$found_file" ]; then
_err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one."
return 1
fi
if ! _check_deployfile "$found_file"; then
_err "Deploy file is not valid: $found_file"
return 1
fi
echo "$found_file"
}
# Description:
# This function checks the deploy file for version compatibility and the existence of the specified configuration and services.
# Arguments:
# $1 - The path to the deploy configuration file.
# $2 - The name of the deploy configuration to use.
# Usage:
# _check_deployfile "<deploy_file_path>"
_check_deployfile() {
_deploy_file="$1"
_debug2 "check: Deploy file" "$_deploy_file"
# Check version
_deploy_file_version=$(yq -r '.version' "$_deploy_file")
if [ "$MULTIDEPLOY_VERSION" != "$_deploy_file_version" ]; then
_err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $_deploy_file_version."
return 1
fi
_debug2 "check: Deploy file version is compatible: $_deploy_file_version"
# Extract all services from config
_services=$(yq -r '.services[].name' "$_deploy_file")
if [ -z "$_services" ]; then
_err "Config does not have any services to deploy to."
return 1
fi
_debug2 "check: Config has services."
echo "$_services" | while read -r _service; do
_debug3 " - $_service"
done
# Check if extracted services exist in services list
echo "$_services" | while read -r _service; do
_debug2 "check: Checking service: $_service"
# Check if service exists
_service_config=$(yq -r ".services[] | select(.name == \"$_service\")" "$_deploy_file")
if [ -z "$_service_config" ] || [ "$_service_config" = "null" ]; then
_err "Service '$_service' not found."
return 1
fi
_service_hook=$(echo "$_service_config" | yq -r ".hook" -)
if [ -z "$_service_hook" ] || [ "$_service_hook" = "null" ]; then
_err "Service '$_service' does not have a hook."
return 1
fi
_service_environment=$(echo "$_service_config" | yq -r ".environment" -)
if [ -z "$_service_environment" ] || [ "$_service_environment" = "null" ]; then
_err "Service '$_service' does not have an environment."
return 1
fi
done
}
# Description: This function takes a list of environment variables in YAML format,
# parses them, and exports each key-value pair as environment variables.
# Arguments:
# $1 - A string containing the list of environment variables in YAML format.
# Usage:
# _export_envs "$env_list"
_export_envs() {
_env_list="$1"
_secure_debug3 "Exporting envs" "$_env_list"
echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do
# Using eval to expand nested variables in the configuration file
_value=$(eval 'echo "'"$_value"'"')
_savedeployconf "$_key" "$_value"
_secure_debug3 "Saved $_key" "$_value"
done
}
# Description:
# This function takes a YAML formatted string of environment variables, parses it,
# and clears each environment variable. It logs the process of clearing each variable.
#
# Note: Environment variables for a hook may be optional and differ between
# services using the same hook.
# If one service sets optional environment variables and another does not, the
# variables may persist and affect subsequent deployments.
# Clearing these variables after each service ensures that only the
# environment variables explicitly specified for each service in the deploy
# file are used.
# Arguments:
# $1 - A YAML formatted string containing environment variable key-value pairs.
# Usage:
# _clear_envs "<yaml_string>"
_clear_envs() {
_env_list="$1"
_secure_debug3 "Clearing envs" "$_env_list"
env_pairs=$(echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value')
echo "$env_pairs" | while IFS='=' read -r _key _value; do
_debug3 "Deleting key" "$_key"
_cleardeployconf "$_key"
unset -v "$_key"
done
}
# Description:
# This function deploys services listed in the deploy configuration file.
# Arguments:
# $1 - The path to the deploy configuration file.
# $2 - The list of services to deploy.
# Usage:
# _deploy_services "<deploy_file_path>" "<services_list>"
_deploy_services() {
_deploy_file="$1"
_debug3 "Deploy file" "$_deploy_file"
_tempfile=$(mktemp)
trap 'rm -f $_tempfile' EXIT
yq -r '.services[].name' "$_deploy_file" >"$_tempfile"
_debug3 "Services" "$(cat "$_tempfile")"
_failedServices=""
_failedCount=0
while read -r _service <&3; do
_debug2 "Service" "$_service"
_hook=$(yq -r ".services[] | select(.name == \"$_service\").hook" "$_deploy_file")
_envs=$(yq -r ".services[] | select(.name == \"$_service\").environment" "$_deploy_file")
_export_envs "$_envs"
if ! _deploy_service "$_service" "$_hook"; then
_failedServices="$_service, $_failedServices"
_failedCount=$((_failedCount + 1))
fi
_clear_envs "$_envs"
done 3<"$_tempfile"
_debug3 "Failed services" "$_failedServices"
_debug2 "Failed count" "$_failedCount"
if [ -n "$_failedServices" ]; then
_info "$(__red "Deployment failed") for services: $_failedServices"
else
_debug "All services deployed successfully."
fi
return "$_failedCount"
}
# Description: Deploys a service using the specified hook.
# Arguments:
# $1 - The name of the service to deploy.
# $2 - The hook to use for deployment.
# Usage:
# _deploy_service <service_name> <hook>
_deploy_service() {
_name="$1"
_hook="$2"
_debug2 "SERVICE" "$_name"
_debug2 "HOOK" "$_hook"
_info "$(__green "Deploying") to '$_name' using '$_hook'"
_deploy "$_cdomain" "$_hook"
}

View file

@ -54,6 +54,8 @@ mydevil_deploy() {
# Usage: ip=$(mydevil_get_ip domain.com)
# echo $ip
mydevil_get_ip() {
devil dns list "$1" | cut -w -s -f 3,7 | grep "^A$(printf '\t')" | cut -w -s -f 2 || return 1
# tr squeezes runs of blanks into one tab so plain cut works everywhere;
# cut -w is BSD-only and unknown to GNU coreutils
devil dns list "$1" | tr -s ' \t' '\t' | cut -s -f 3,7 | grep "^A$(printf '\t')" | cut -s -f 2 || return 1
return 0
}

View file

@ -68,8 +68,8 @@ deployer() {
# Get Version Info to test key
content="type=version&key=$_panos_key"
## Exclude all scopes for the empty commit
#_exclude_scope="<policy-and-objects>exclude</policy-and-objects><device-and-network>exclude</device-and-network><shared-object>exclude</shared-object>"
#content="type=commit&action=partial&key=$_panos_key&cmd=<commit><partial>$_exclude_scope<admin><member>acmekeytest</member></admin></partial></commit>"
#_exclude_scope="<device-and-network>excluded</device-and-network><shared-object>excluded</shared-object>"
#content="type=commit&action=partial&key=$_panos_key&cmd=<commit><partial>$_exclude_scope<admin><member>$_panos_user</member></admin></partial></commit>"
fi
# Generate API Key
@ -128,10 +128,9 @@ deployer() {
#Check for force commit - will commit ALL uncommited changes to the firewall. Use with caution!
if [ "$FORCE" ]; then
_debug "Force switch detected. Committing ALL changes to the firewall."
cmd=$(printf "%s" "<commit><partial><force><admin><member>$_panos_user</member></admin></force></partial></commit>" | _url_encode)
cmd=$(printf "%s" "<commit><force><partial><admin><member>$_panos_user</member></admin></partial></force></commit>" | _url_encode)
else
_exclude_scope="<policy-and-objects>exclude</policy-and-objects><device-and-network>exclude</device-and-network>"
cmd=$(printf "%s" "<commit><partial>$_exclude_scope<admin><member>$_panos_user</member></admin></partial></commit>" | _url_encode)
cmd=$(printf "%s" "<commit><partial><admin><member>$_panos_user</member></admin></partial></commit>" | _url_encode)
fi
content="type=commit&action=partial&key=$_panos_key&cmd=$cmd"
fi
@ -207,13 +206,12 @@ panos_deploy() {
fi
# PANOS_KEY
_getdeployconf PANOS_KEY
if [ "$PANOS_KEY" ]; then
_debug "Detected saved key."
_panos_key=$PANOS_KEY
_debug "Detected ENV variable PANOS_KEY. Saving to file."
_savedeployconf PANOS_KEY "$PANOS_KEY" 1
else
_debug "No key detected"
unset _panos_key
_debug "Attempting to load variable PANOS_KEY from file."
_getdeployconf PANOS_KEY
fi
# PANOS_TEMPLATE
@ -256,6 +254,7 @@ panos_deploy() {
_panos_host=$PANOS_HOST
_panos_user=$PANOS_USER
_panos_pass=$PANOS_PASS
_panos_key=$PANOS_KEY
_panos_template=$PANOS_TEMPLATE
_panos_template_stack=$PANOS_TEMPLATE_STACK
_panos_vsys=$PANOS_VSYS
@ -271,12 +270,6 @@ panos_deploy() {
if [ -z "$_panos_host" ]; then
_err "No host found. If this is your first time deploying, please set PANOS_HOST in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
elif [ -z "$_panos_user" ]; then
_err "No user found. If this is your first time deploying, please set PANOS_USER in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
elif [ -z "$_panos_pass" ]; then
_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
else
# Use certificate name based on the first domain on the certificate if no custom certificate name is set
if [ -z "$_panos_certname" ]; then
@ -286,6 +279,13 @@ panos_deploy() {
# Generate a new API key if no valid API key is found
if [ -z "$_panos_key" ]; then
if [ -z "$_panos_user" ]; then
_err "No user found. If this is your first time deploying, please set PANOS_USER in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
elif [ -z "$_panos_pass" ]; then
_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
fi
_debug "**** Generating new PANOS API KEY ****"
deployer keygen
_savedeployconf PANOS_KEY "$_panos_key" 1
@ -296,9 +296,20 @@ panos_deploy() {
_err "Unable to generate an API key. The user and pass may be invalid or not authorized to generate a new key. Please check the PANOS_USER and PANOS_PASS credentials and try again"
return 1
else
deployer cert
deployer key
deployer commit
# A commit of a failed import would leave a mismatched cert/key pair
# on the firewall and can lock the admin out of the management
# interface, see https://github.com/acmesh-official/acme.sh/issues/4716
if ! deployer cert; then
_err "Cert import failed. Aborting without committing."
return 1
fi
if ! deployer key; then
_err "Key import failed. Aborting without committing. Warning: the firewall now has an uncommitted mismatched cert/key pair in its candidate config."
return 1
fi
if ! deployer commit; then
return 1
fi
if [ "$_panos_template_stack" ]; then
# try to get job status for 20 times in 30 sec interval
i=0

View file

@ -117,14 +117,23 @@ HEREDOC
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
if [ "${_retval}" -eq 0 ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
else
_err "Certificate deployment failed"
_debug "Response" "$response"
return 1
fi
# The API errors out with a non-2xx HTTP status and an empty body,
# so the status line is checked too, not only the response body.
_status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 "HTTP status" "$_status_code"
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
case "$_status_code" in
2[0-9][0-9])
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
fi
;;
esac
_err "Certificate deployment failed (HTTP status $_status_code). $message"
_debug "Response" "$response"
return 1
}

View file

@ -129,14 +129,23 @@ HEREDOC
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
if [ "${_retval}" -eq 0 ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
else
_err "Certificate deployment failed"
_debug "Response" "$response"
return 1
fi
# The API errors out with a non-2xx HTTP status and an empty body,
# so the status line is checked too, not only the response body.
_status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 "HTTP status" "$_status_code"
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
case "$_status_code" in
2[0-9][0-9])
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
fi
;;
esac
_err "Certificate deployment failed (HTTP status $_status_code). $message"
_debug "Response" "$response"
return 1
}

View file

@ -8,6 +8,8 @@
# 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"
# Optional: force HTTPS redirect (default: false)
# export QINIU_FORCE_HTTPS="true"
QINIU_API_BASE="https://api.qiniu.com"
@ -44,6 +46,12 @@ qiniu_deploy() {
QINIU_CDN_DOMAIN="$_cdomain"
fi
if [ -z "$QINIU_FORCE_HTTPS" ]; then
QINIU_FORCE_HTTPS="false"
else
_savedomainconf QINIU_FORCE_HTTPS "$QINIU_FORCE_HTTPS"
fi
## upload certificate
string_fullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
@ -69,7 +77,7 @@ qiniu_deploy() {
_debug certId "$_certId"
## update domain ssl config
update_body="{\"certid\":$_certId,\"forceHttps\":false}"
update_body="{\"certid\":$_certId,\"forceHttps\":$QINIU_FORCE_HTTPS}"
for domain in $QINIU_CDN_DOMAIN; do
update_path="/domain/$domain/httpsconf"
update_access_token="$(_make_access_token "$update_path")"

View file

@ -125,7 +125,7 @@ routeros_deploy() {
_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"
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES" "base64"
# push key to routeros
if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then
@ -143,6 +143,7 @@ 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 ];\
\n/certificate remove [ find name=$_cdomain.cer_3 ];\
\ndelay 1;\
\n/certificate import file-name=\\\"$_cdomain.cer\\\" passphrase=\\\"\\\";\
\n/certificate import file-name=\\\"$_cdomain.key\\\" passphrase=\\\"\\\";\

280
deploy/shelly.sh Normal file
View file

@ -0,0 +1,280 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to a Shelly Gen3+ device.
# Deploy the HTTPS server certificate to a Shelly device on the local network.
#
# ```sh
# export SHELLY_HOST=192.168.1.100
# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
# acme.sh --deploy -d shelly.example.com --deploy-hook shelly
# ```
#
# Environment variables:
# SHELLY_HOST (required) IP or hostname of the Shelly device
# SHELLY_PASSWORD (optional) Admin password for digest authentication.
# Omit if auth is disabled on the device.
# SHELLY_USER (optional) Username for auth. Default: admin
# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot.
# Default: 1 (reboot after upload)
#
# Requirements:
# - Shelly Gen3+ device (Gen4 recommended)
# - Firmware 2.0.0+ for HTTPS server certificate support
# - curl or wget
# - openssl (for SHA-256 digest and random cnonce)
#
# The device must be reachable via HTTP on the local network.
# The hook uploads the fullchain.pem and private key,
# then reboots the device to apply the new certificate.
#
# Authentication uses standard RFC 7616 HTTP Digest (SHA-256) since
# firmware 2.0.0. The JSON-RPC auth object is not used for HTTP transport.
#
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
shelly_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf SHELLY_HOST
_getdeployconf SHELLY_PASSWORD
_getdeployconf SHELLY_USER
_getdeployconf SHELLY_REBOOT
_debug SHELLY_HOST "$SHELLY_HOST"
_debug SHELLY_USER "$SHELLY_USER"
_secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD"
_debug SHELLY_REBOOT "$SHELLY_REBOOT"
if [ -z "$SHELLY_HOST" ]; then
_err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device."
return 1
fi
SHELLY_USER="${SHELLY_USER:-admin}"
SHELLY_REBOOT="${SHELLY_REBOOT:-1}"
_savedeployconf SHELLY_HOST "$SHELLY_HOST"
_savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD"
_savedeployconf SHELLY_USER "$SHELLY_USER"
_savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT"
# --- Auth handshake (only if password is set) ---
_shelly_auth_header=""
if [ -n "$SHELLY_PASSWORD" ]; then
_info "Authenticating to Shelly device at $SHELLY_HOST"
if ! _shelly_handshake; then
_err "Authentication handshake failed. Check SHELLY_PASSWORD and device accessibility."
return 1
fi
_info "Authentication successful"
fi
# --- Upload certificate ---
_info "Uploading certificate to Shelly device at $SHELLY_HOST"
if ! _shelly_upload_cert; then
_err "Certificate upload failed"
return 1
fi
# --- Upload key ---
_info "Uploading private key to Shelly device"
if ! _shelly_upload_key; then
_err "Private key upload failed"
return 1
fi
_info "Certificate and key uploaded successfully"
# --- Reboot ---
if [ "$SHELLY_REBOOT" != "0" ]; then
_info "Rebooting Shelly device to apply certificate"
# Reboot may close the connection before sending a response
_shelly_rpc "Shelly.Reboot" '{}' || _debug "Reboot may have closed connection (expected)"
_info "Reboot command sent. Device will restart shortly."
else
_info "Skipping reboot (SHELLY_REBOOT=0). Certificate will apply on next restart."
fi
# Clear auth header so it does not leak to other hooks
export _H1=""
return 0
}
# --- Helper functions ---
# Perform RFC 7616 HTTP Digest auth handshake.
# Sets _shelly_auth_header on success (the Authorization header value).
_shelly_handshake() {
_inithttp
_debug "Probing device for auth challenge"
# Use a protected method (Shelly.GetStatus) to trigger 401.
# Shelly.GetDeviceInfo is excluded from auth and would miss the challenge.
_post '{"id":1,"method":"Shelly.GetStatus"}' \
"http://${SHELLY_HOST}/rpc" "" "" "application/json"
# Detect auth from HTTP status line rather than response body
if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then
# No auth challenge — device accepted the request without credentials
_debug "Device responded without auth challenge. Proceeding without auth."
return 0
fi
_shelly_realm="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*realm="//;s/".*//')"
_shelly_nonce="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*nonce="//;s/".*//')"
_shelly_qop="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*qop="//;s/".*//')"
if [ -z "$_shelly_nonce" ]; then
_err "Failed to extract nonce from WWW-Authenticate header. Is SHELLY_PASSWORD correct?"
return 1
fi
_shelly_qop="${_shelly_qop:-auth}"
_debug "Shelly realm: $_shelly_realm"
_debug "Shelly qop: $_shelly_qop"
_secure_debug "Shelly nonce" "$_shelly_nonce"
# ha1 = SHA256(username:realm:password)
_shelly_ha1="$(printf '%s' "${SHELLY_USER}:${_shelly_realm}:${SHELLY_PASSWORD}" | _digest sha256 hex)"
_secure_debug "Shelly ha1" "$_shelly_ha1"
# Generate client nonce (openssl is required for _digest, so always available)
_shelly_cnonce="$(${ACME_OPENSSL_BIN:-openssl} rand -hex 8 2>/dev/null)"
_debug "Shelly cnonce: $_shelly_cnonce"
# Build the digest Authorization header value (stored for reuse)
_shelly_nc=1
_shelly_build_auth_header
return 0
}
# Check whether the HTTP response headers contain a digest auth challenge.
# Returns 0 (true) if a 401 with WWW-Authenticate is present.
_shelly_has_auth_challenge() {
_shelly_headers_file="$1"
_shelly_status="$(grep -i '^HTTP/' "$_shelly_headers_file" | _tail_n 1 | awk '{print $2}')"
[ "$_shelly_status" = "401" ] && grep -qi '^WWW-Authenticate:' "$_shelly_headers_file"
}
# Build or rebuild the RFC 7616 Authorization header.
# Uses: _shelly_ha1, _shelly_nonce, _shelly_cnonce, _shelly_qop, _shelly_realm, _shelly_nc
# Sets: _shelly_auth_header
_shelly_build_auth_header() {
_shelly_nc_hex="$(printf '%08x' "$_shelly_nc")"
# ha2 = SHA256(POST:/rpc)
_shelly_ha2="$(printf '%s' "POST:/rpc" | _digest sha256 hex)"
# response = SHA256(ha1:nonce:nc:cnonce:qop:ha2)
_shelly_digest_response="$(printf '%s' "${_shelly_ha1}:${_shelly_nonce}:${_shelly_nc_hex}:${_shelly_cnonce}:${_shelly_qop}:${_shelly_ha2}" | _digest sha256 hex)"
# Build the Authorization header value (without the "Authorization: " prefix)
_shelly_auth_header="Digest username=\"${SHELLY_USER}\", realm=\"${_shelly_realm}\", nonce=\"${_shelly_nonce}\", uri=\"/rpc\", qop=${_shelly_qop}, nc=${_shelly_nc_hex}, cnonce=\"${_shelly_cnonce}\", response=\"${_shelly_digest_response}\", algorithm=SHA-256"
_secure_debug "Authorization header" "$_shelly_auth_header"
}
# Make a Shelly JSON-RPC call.
# Usage: _shelly_rpc <method> <params_json>
# Returns 0 on success, 1 on error.
_shelly_rpc() {
_shelly_method="$1"
_shelly_params="$2"
_shelly_body='{"id":1,"method":"'"$_shelly_method"'","params":'"$_shelly_params"'}'
_debug "RPC method: $_shelly_method"
_debug2 "RPC body: $_shelly_body"
# shellcheck disable=SC2090
if [ -n "$_shelly_auth_header" ]; then
export _H1="Authorization: $_shelly_auth_header"
else
export _H1=""
fi
_post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json"
_shelly_ret=$?
if [ "$_shelly_ret" != "0" ]; then
_err "HTTP request failed for $_shelly_method (curl/wget error $_shelly_ret)"
return 1
fi
# Empty response means something went wrong (auth required but not provided, etc.)
if [ -z "$response" ]; then
_err "Empty response from Shelly device. If authentication is enabled on the device, set SHELLY_PASSWORD."
return 1
fi
# Validate response looks like a Shelly JSON-RPC response.
# Catches non-JSON responses such as HTTP 429 "Too Many Requests" which
# would otherwise pass the empty and "error" checks below.
if ! _startswith "$response" '{' || ! _contains "$response" '"id"'; then
_err "Invalid response from Shelly device: $response"
return 1
fi
# Check for JSON-RPC error in response
if _contains "$response" '"error"'; then
_err "RPC error from Shelly: $response"
return 1
fi
_debug "RPC response: $response"
# Increment nonce counter and rebuild auth header for next request
if [ -n "$_shelly_auth_header" ]; then
_shelly_nc=$((_shelly_nc + 1))
_shelly_build_auth_header
fi
return 0
}
# Upload the certificate to the device.
# Note: We do NOT clear the existing certificate first, because the Shelly
# auto-removes all three files (cert, key, CA) when any one is cleared.
# Uploading overwrites in place — no clearing needed.
_shelly_upload_cert() {
_shelly_cert_data="$(_json_encode <"$_cfullchain")"
_debug "Uploading certificate"
if ! _shelly_rpc "Shelly.PutHTTPServerCert" '{"data":"'"$_shelly_cert_data"'"}'; then
_err "Failed to upload certificate to device"
return 1
fi
return 0
}
# Upload the private key to the device.
# Note: Do not clear first — see _shelly_upload_cert for rationale.
_shelly_upload_key() {
_shelly_key_data="$(_json_encode <"$_ckey")"
_debug "Uploading key"
if ! _shelly_rpc "Shelly.PutHTTPServerKey" '{"data":"'"$_shelly_key_data"'"}'; then
_err "Failed to upload key to device"
return 1
fi
return 0
}

View file

@ -25,7 +25,8 @@
# export DEPLOY_SSH_MULTI_CALL="" # yes or no, default to no or previously saved value
# export DEPLOY_SSH_USE_SCP="" yes or no, default to no
# export DEPLOY_SSH_SCP_CMD="" defaults to "scp -q"
#
# export DEPLOY_SSH_REMOTE_SHELL="" # defaults to sh -c
# export DEPLOY_SSH_REMOTE_CMD_QUOTE="" # yes or no, defaults to yes
######## Public functions #####################
#domain keyfile certfile cafile fullchain
@ -71,6 +72,24 @@ ssh_deploy() {
fi
_savedeployconf DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD"
# REMOTE_SHELL is optional. If not provided then use sh
_migratedeployconf Le_Deploy_ssh_remote_shell DEPLOY_SSH_REMOTE_SHELL
_getdeployconf DEPLOY_SSH_REMOTE_SHELL
_debug2 DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL"
if [ -z "$DEPLOY_SSH_REMOTE_SHELL" ]; then
DEPLOY_SSH_REMOTE_SHELL="sh -c"
fi
_savedeployconf DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL"
# REMOTE_CMD_QUOTE is optional. If not provided then yes
_migratedeployconf Le_Deploy_ssh_remote_cmd_quote DEPLOY_SSH_REMOTE_CMD_QUOTE
_getdeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE
_debug2 DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE"
if [ -z "$DEPLOY_SSH_REMOTE_CMD_QUOTE" ]; then
DEPLOY_SSH_REMOTE_CMD_QUOTE="yes"
fi
_savedeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE"
# BACKUP is optional. If not provided then default to previously saved value or yes.
_migratedeployconf Le_Deploy_ssh_backup DEPLOY_SSH_BACKUP
_getdeployconf DEPLOY_SSH_BACKUP
@ -170,10 +189,16 @@ ssh_deploy() {
_info "Required commands batched and sent in single call to remote host"
fi
_returnCode=0
_deploy_ssh_servers="$DEPLOY_SSH_SERVER"
for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do
_ssh_deploy
if ! _ssh_deploy; then
# in case of an error, remember it, but keep going for the remaining servers
_returnCode=1
fi
done
return $_returnCode
}
_ssh_deploy() {
@ -238,6 +263,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
return $_err_code
fi
else
# If file doesn't exist, create it and change its permissions.
_cmdstr="$_cmdstr test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE;"
# ssh echo to the file
_cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;"
_info "will copy private key to remote file $DEPLOY_SSH_KEYFILE"
@ -426,9 +453,13 @@ _ssh_remote_cmd() {
_secure_debug "Remote commands to execute: $_cmd"
_info "Submitting sequence of commands to remote server by $_ssh_cmd"
# quotations in bash cmd below intended. Squash travis spellcheck error
# shellcheck disable=SC2029
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" sh -c "'$_cmd'"
if [ "$DEPLOY_SSH_REMOTE_CMD_QUOTE" = "yes" ]; then
# quotations in bash cmd below intended. Squash travis spellcheck error
# shellcheck disable=SC2029
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "'$_cmd'"
else
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "$_cmd"
fi
_err_code="$?"
if [ "$_err_code" != "0" ]; then

View file

@ -33,7 +33,7 @@ strongswan_deploy() {
return 1
fi
_info _confdir "${_confdir}"
__deploy_cert "$@" "stroke" "${_confdir}"
__deploy_cert "stroke" "${_confdir}" "$@"
${_ipsec} reload
fi
# For modern vici mode
@ -50,7 +50,7 @@ strongswan_deploy() {
_err "no swanctl config dir is found"
return 1
fi
__deploy_cert "$@" "vici" "${_confdir}"
__deploy_cert "vici" "${_confdir}" "$@"
${_swanctl} --load-creds
fi
if [ -z "${_swanctl}" ] && [ -z "${_ipsec}" ]; then
@ -63,13 +63,13 @@ strongswan_deploy() {
#################### Private functions below ##################################
__deploy_cert() {
_cdomain="${1}"
_ckey="${2}"
_ccert="${3}"
_cca="${4}"
_cfullchain="${5}"
_swan_mode="${6}"
_confdir="${7}"
_swan_mode="${1}"
_confdir="${2}"
_cdomain="${3}"
_ckey="${4}"
_ccert="${5}"
_cca="${6}"
_cfullchain="${7}"
_debug _cdomain "${_cdomain}"
_debug _ckey "${_ckey}"
_debug _ccert "${_ccert}"

View file

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env sh
################################################################################
# ACME.sh 3rd party deploy plugin for Synology DSM
@ -72,7 +72,7 @@ synology_dsm_deploy() {
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
if ! _exists synouser || ! _exists synogroup || ! _exists synosetkeyvalue; then
_err "Missing required tools to creat temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
_err "Missing required tools to create temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
_err "Notice: temp admin user authorization method only supports local deployment on DSM."
return 1
fi
@ -234,11 +234,11 @@ synology_dsm_deploy() {
fi
fi
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
_debug2 error_code "$error_code"
# Account has 2FA-OTP enabled, since error 403 reported.
# https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf
if [ "$error_code" == "403" ]; then
if [ "$error_code" = "403" ]; then
if [ -z "$SYNO_DEVICE_NAME" ]; then
printf "Enter device name or leave empty for default (CertRenewal): "
read -r SYNO_DEVICE_NAME
@ -269,27 +269,27 @@ synology_dsm_deploy() {
_secure_debug2 SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
fi
fi
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
_debug2 error_code "$error_code"
fi
if [ -n "$error_code" ]; then
if [ "$error_code" == "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
if [ "$error_code" = "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
_cleardeployconf SYNO_DEVICE_ID
_err "Failed to authenticate with SYNO_DEVICE_ID (may expired or invalid), please try again in a new terminal window."
elif [ "$error_code" == "404" ]; then
_err "Failed to authenticate with SYNO_DEVICE_ID (may be expired or invalid), please try again in a new terminal window."
elif [ "$error_code" = "404" ]; then
_err "Failed to authenticate with provided 2FA-OTP code, please try again in a new terminal window."
elif [ "$error_code" == "406" ]; then
elif [ "$error_code" = "406" ]; then
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
_err "Failed with unexcepted error, please report this by providing full log with '--debug 3'."
else
_err "Enforce auth with 2FA-OTP enabled, please configure the user to enable 2FA-OTP to continue."
fi
elif [ "$error_code" == "400" ]; then
elif [ "$error_code" = "400" ]; then
_err "Failed to authenticate, no such account or incorrect password."
elif [ "$error_code" == "401" ]; then
elif [ "$error_code" = "401" ]; then
_err "Failed to authenticate with a non-existent account."
elif [ "$error_code" == "408" ] || [ "$error_code" == "409" ] || [ "$error_code" == "410" ]; then
elif [ "$error_code" = "408" ] || [ "$error_code" = "409" ] || [ "$error_code" = "410" ]; then
_err "Failed to authenticate, the account password has expired or must be changed."
else
_err "Failed to authenticate with error: $error_code."
@ -322,8 +322,8 @@ synology_dsm_deploy() {
_savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
_savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME"
else
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME"
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD"
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME" "base64"
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" "base64"
_savedeployconf SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
_savedeployconf SYNO_DEVICE_NAME "$SYNO_DEVICE_NAME"
fi
@ -336,7 +336,7 @@ synology_dsm_deploy() {
id=$(echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\"id\":\"\([^\"]*\).*/\1/p")
_debug2 id "$id"
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
_debug2 error_code "$error_code"
if [ -n "$error_code" ]; then
if [ "$error_code" -eq 105 ]; then
@ -344,6 +344,7 @@ synology_dsm_deploy() {
else
_err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more."
fi
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
@ -353,7 +354,8 @@ synology_dsm_deploy() {
_debug2 SYNO_CREATE "$SYNO_CREATE"
if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then
_err "Unable to find certificate: $SYNO_CERTIFICATE and $SYNO_CREATE is not set."
_err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set."
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
@ -387,15 +389,15 @@ synology_dsm_deploy() {
if echo "$response" | grep '"restart_httpd":true' >/dev/null; then
_info "Restart HTTP services succeeded."
else
_info "Restart HTTP services failed."
_info "Restart HTTP services not necessary."
fi
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 0
else
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_err "Unable to update certificate, got error response: $response."
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
}
@ -403,6 +405,8 @@ synology_dsm_deploy() {
#################### Private functions below ##################################
_logout() {
# Logout CERT user only to not occupy a permanent session, e.g. in DSM's "Connected Users" widget (based on previous variables)
# Must be called before _temp_admin_cleanup: once the temp admin is deleted, its session can no longer be logged out.
# Note: this overwrites $response, so print any error message that needs it before calling.
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid")
_debug3 response "$response"
}
@ -424,11 +428,6 @@ _temp_admin_cleanup() {
fi
}
#_cleardeployconf key
_cleardeployconf() {
_cleardomainconf "SAVED_$1"
}
# key
_check2cleardeployconfexp() {
_key="$1"

View file

@ -16,7 +16,12 @@
#
# # API KEY
# # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI>"
# Optional:
# export DEPLOY_TRUENAS_HOSTNAME="<TRUENAS_HOSTNAME_OR_IP>"
# export DEPLOY_TRUENAS_PROTOCOL="wss" # ws or wss
# export DEPLOY_TRUENAS_PORT="443" # optional, e.g. 80, 443, 8443
#
### Private functions
@ -56,7 +61,6 @@ _ws_call() {
_ws_upload_cert() {
/usr/bin/env python - <<EOF
import sys
from truenas_api_client import Client
@ -78,7 +82,6 @@ with Client(uri="$_ws_uri") as c:
print("R:0")
print("E:_ws_upload_cert error!")
sys.exit(7)
EOF
return $?
@ -181,6 +184,8 @@ truenas_ws_deploy() {
_getdeployconf DEPLOY_TRUENAS_APIKEY
_getdeployconf DEPLOY_TRUENAS_HOSTNAME
_getdeployconf DEPLOY_TRUENAS_PROTOCOL
_getdeployconf DEPLOY_TRUENAS_PORT
# Check API Key
if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
_err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
@ -196,7 +201,21 @@ truenas_ws_deploy() {
_info "TrueNAS protocol not set. Using 'ws'."
DEPLOY_TRUENAS_PROTOCOL="ws"
fi
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
# Check port, optional
if [ -n "$DEPLOY_TRUENAS_PORT" ]; then
case "$DEPLOY_TRUENAS_PORT" in
'' | *[!0-9]*)
_err "Invalid TrueNAS port '$DEPLOY_TRUENAS_PORT'. DEPLOY_TRUENAS_PORT must be numeric."
return 8
;;
esac
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME:$DEPLOY_TRUENAS_PORT/websocket"
else
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
fi
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
_debug _ws_uri "$_ws_uri"
@ -216,13 +235,14 @@ truenas_ws_deploy() {
if [ "$_ws_response" != "TRUE" ]; then
_err "TrueNAS is not ready."
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL."
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME, DEPLOY_TRUENAS_PROTOCOL and DEPLOY_TRUENAS_PORT."
_err "Verify API key."
return 2
fi
_savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
_savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_savedeployconf DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
_savedeployconf DEPLOY_TRUENAS_PORT "$DEPLOY_TRUENAS_PORT"
_info "TrueNAS health: OK"
########## System info

307
deploy/unifios.sh Normal file
View file

@ -0,0 +1,307 @@
#!/usr/bin/env sh
# Deploy hook for UniFi OS Server (self-hosted).
#
# Supports:
# - UniFi OS Server on macOS
# - UniFi OS Server on Linux
# - UniFi OS Server on Windows should also work (runs under WSL2), but
# has not been tested.
#
# Tested on: Ubuntu 26.04 (remote) and macOS 26.6 (local).
#
# This is a different product from the Cloud Key / UDM hardware and
# self-hosted Unifi Controller covered by the `unifi` deploy hook above
# (that hook already covers Cloud Key running UnifiOS v2.0.0+/Gen2/2+) --
# this hook targets the separately-installed, self-hosted "UniFi OS Server"
# application instead, which stores certificates in its own Postgres
# database via a REST API rather than a Java keystore, so the `unifi`
# hook's approach does not apply here.
#
# UniFi OS Server exposes a REST API on its management port (default
# 11443) that its own web UI uses for certificate management:
# POST /api/auth/login - session login (cookie + JWT)
# GET /api/userCertificates - list uploaded certificates
# POST /api/userCertificates - upload a new certificate
# DELETE /api/userCertificates/{id} - remove a certificate
# PUT /api/userCertificates/{id}/status - activate/deactivate a certificate
#
# This was reverse-engineered from the browser's Network tab while using the
# real GUI upload/activate/delete flow -- it is undocumented but is the same
# code path the UI uses, so it's far more robust than editing settings.yaml,
# http/local-certs.conf, or the underlying Postgres user_certificates table
# directly (all of which are also touched by this API, but only as a result
# of the app's own internal logic, which handles cert parsing, active-cert
# bookkeeping, and nginx config regeneration correctly on its own).
#
# Auth: POST /api/auth/login returns a `TOKEN` cookie containing a JWT whose
# payload has a `csrfToken` claim. That value must be echoed back as the
# `x-csrf-token` header on every subsequent state-changing request (a classic
# double-submit CSRF pattern). No other cookies were found to be necessary.
#
# Uses core acme.sh helpers throughout (_post/_get, _json_encode,
# _durl_replace_base64, _dbase64, _egrep_o) rather than raw curl -k or
# python3, so the wget fallback, --debug tracing, and CA_BUNDLE are all
# honored the same as every other hook. The management API's cert is
# self-signed (it's a management-only port, not meant for public exposure),
# so this hook sets HTTPS_INSECURE=1 itself, scoped to its own subshell (see
# acme.sh's per-hook sourcing in _deploy) -- it does not weaken TLS
# verification for the rest of the acme.sh run, e.g. the connection to the
# ACME CA.
#
# Design: This hook does not save a certificate ID between renewals. Each
# upload gets a name unique to that run: the domain name plus a timestamp.
# This name never collides with an entry from a previous deploy. This is
# true even if that entry is still active. The hook uploads and activates
# the new certificate before it removes any old entries. If a failure
# occurs during this process, the server still has a valid, active
# certificate. The hook removes old entries only after activation is
# complete. It removes only entries whose name starts with the domain name,
# because this is the hook's own naming convention. As a result, this step
# can only affect entries that this hook created for this domain. It can
# never affect a certificate that a user uploaded manually, and it can
# never affect a self-signed certificate.
#
# Settings:
# DEPLOY_UNIFIOS_HOST - base URL of the management API
# (default: "https://localhost:11443")
# DEPLOY_UNIFIOS_USERNAME - UniFi OS Server admin username (required)
# DEPLOY_UNIFIOS_PASSWORD - UniFi OS Server admin password (required)
#
# Example:
# export DEPLOY_UNIFIOS_USERNAME="acmeuser"
# export DEPLOY_UNIFIOS_PASSWORD="xxxxx"
# acme.sh --deploy -d example.com --deploy-hook unifios
#
# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/7182
_uos_response_code() {
# tr strips the trailing newline along with form feeds; re-terminate
# before the second _egrep_o, whose sed fallback (used wherever egrep -o
# is unavailable) drops an unterminated final line on some platforms.
_uos_code="$(_egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n")"
printf '%s\n' "$_uos_code" | _egrep_o "^[0-9][0-9]*"
}
_uos_response_cookie() {
# $1 = cookie name
grep <"$HTTP_HEADER" -i "^Set-Cookie: *$1=" | _tail_n 1 | _egrep_o "$1=[^;]*" | _head_n 1
}
unifios_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# Scoped to this hook's own subshell -- does not affect the rest of the
# acme.sh run (e.g. the connection to the ACME CA).
export HTTPS_INSECURE=1
_getdeployconf DEPLOY_UNIFIOS_HOST
DEPLOY_UNIFIOS_HOST="${DEPLOY_UNIFIOS_HOST:-https://localhost:11443}"
_savedeployconf DEPLOY_UNIFIOS_HOST "$DEPLOY_UNIFIOS_HOST"
_debug DEPLOY_UNIFIOS_HOST "$DEPLOY_UNIFIOS_HOST"
_getdeployconf DEPLOY_UNIFIOS_USERNAME
_getdeployconf DEPLOY_UNIFIOS_PASSWORD
if [ -z "$DEPLOY_UNIFIOS_USERNAME" ] || [ -z "$DEPLOY_UNIFIOS_PASSWORD" ]; then
_err "DEPLOY_UNIFIOS_USERNAME and DEPLOY_UNIFIOS_PASSWORD must be set."
return 1
fi
_debug DEPLOY_UNIFIOS_USERNAME "$DEPLOY_UNIFIOS_USERNAME"
_secure_debug DEPLOY_UNIFIOS_PASSWORD "$DEPLOY_UNIFIOS_PASSWORD"
_info "Logging in to UniFi OS Server API at $DEPLOY_UNIFIOS_HOST..."
# _json_encode always appends a trailing "\n" escape, even to input with
# no trailing newline (it normalizes via `echo`, unconditionally adding
# one). That's harmless for the key/cert file content below, which
# legitimately ends in a real newline anyway, but wrong for these plain
# strings -- strip the spurious escape it leaves behind.
_uos_user_json="$(printf '%s' "$DEPLOY_UNIFIOS_USERNAME" | _json_encode)"
_uos_user_json="${_uos_user_json%\\n}"
_uos_pass_json="$(printf '%s' "$DEPLOY_UNIFIOS_PASSWORD" | _json_encode)"
_uos_pass_json="${_uos_pass_json%\\n}"
_login_body="{\"username\":\"$_uos_user_json\",\"password\":\"$_uos_pass_json\",\"token\":\"\",\"rememberMe\":false}"
_login_json="$(_post "$_login_body" "$DEPLOY_UNIFIOS_HOST/api/auth/login" "" "POST" "application/json")"
_login_code="$(_uos_response_code)"
if [ "$_login_code" != "200" ]; then
_err "Login failed (HTTP $_login_code)."
_err "Response: $_login_json"
return 1
fi
# Credentials are proven correct now -- save them, rather than only at the
# very end, so a later step failing doesn't discard a working login.
# base64-encoded: _save_conf wraps values in single quotes with no
# escaping, so a literal "'" in the password would otherwise corrupt the
# domain conf (see deploy/synology_dsm.sh for the same pattern).
_savedeployconf DEPLOY_UNIFIOS_USERNAME "$DEPLOY_UNIFIOS_USERNAME" "base64"
_savedeployconf DEPLOY_UNIFIOS_PASSWORD "$DEPLOY_UNIFIOS_PASSWORD" "base64"
_uos_token="$(_uos_response_cookie TOKEN)"
if [ -z "$_uos_token" ]; then
_err "Login succeeded but no TOKEN cookie was returned."
return 1
fi
_H1="Cookie: $_uos_token"
export _H1
_uos_jwt_payload="$(echo "$_uos_token" | cut -d '=' -f 2- | cut -d '.' -f 2)"
_uos_csrf="$(_durl_replace_base64 "$_uos_jwt_payload" | _dbase64 | _egrep_o '"csrfToken":"[^"]*"' | cut -d '"' -f 4)"
if [ -z "$_uos_csrf" ]; then
_err "Could not extract csrfToken from session token."
return 1
fi
_H2="x-csrf-token: $_uos_csrf"
export _H2
_info "Uploading new certificate..."
# "name" is a purely cosmetic label -- the server never validates it
# against the certificate's actual CN/SAN, and accepts arbitrary text
# including spaces (confirmed: a cert for example.com served correctly
# after being uploaded under the unrelated name "totally unrelated label").
# The only constraint that matters here is uniqueness: the server rejects
# a second entry with a name it already has, so a bare domain name would
# collide with the previous deploy's entry on every renewal after the
# first. A full human-readable timestamp would make that obvious in the
# UI, but the certificate list's name column is fixed-width and doesn't
# wrap (confirmed against the real UI: a long name overlaps the Expires
# column and makes both unreadable), so keep the suffix short instead --
# Unix epoch seconds are still unique enough for this purpose.
_uos_name="$_cdomain $(_time)"
_uos_key_json="$(_json_encode <"$_ckey")"
_uos_cert_json="$(_json_encode <"$_cfullchain")"
_create_body="{\"name\":\"$_uos_name\",\"key\":\"$_uos_key_json\",\"cert\":\"$_uos_cert_json\"}"
_create_json="$(_post "$_create_body" "$DEPLOY_UNIFIOS_HOST/api/userCertificates" "" "POST" "application/json")"
_create_code="$(_uos_response_code)"
if [ "$_create_code" = "201" ]; then
_new_id="$(echo "$_create_json" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
if [ -z "$_new_id" ]; then
_err "Could not determine new certificate ID from upload response."
return 1
fi
elif [ "$_create_code" = "400" ] && echo "$_create_json" | grep -q "USER_CERTIFICATE_DUPLICATE"; then
# HTTP 400 alone just means "bad request" -- it's the USER_CERTIFICATE_DUPLICATE
# code in the response body, checked above, that actually confirms this.
# The name above is unique to this run, so a duplicate here can only be
# the server's other uniqueness constraint: this exact certificate (by
# fingerprint) already exists as some other entry -- most likely a retry
# after a prior run already uploaded it (a real renewal always produces a
# new fingerprint, so this shouldn't happen in normal cron use). The
# response body doesn't include the existing entry's id, so look it up
# by fingerprint instead.
# The API's own fingerprint field is SHA-1 (20 bytes), not SHA-256 --
# confirmed against a real response, e.g.
# "fingerprint":"FC:02:50:9C:3B:3F:B7:79:9D:CA:4D:7C:AC:92:E7:D5:EA:F1:3A:29"
# (20 colon-separated groups). _fingerprint (core helper) strips the
# colons that field has, so re-insert them rather than stripping the
# JSON's own colons, which would also remove the ones separating every
# key from its value.
_uos_fingerprint="$(_fingerprint "$_cfullchain" sha1)"
if [ -z "$_uos_fingerprint" ]; then
_err "Could not compute the certificate's fingerprint."
return 1
fi
_uos_fingerprint="$(echo "$_uos_fingerprint" | sed 's/\(..\)/\1:/g; s/:$//')"
_list_json="$(_get "$DEPLOY_UNIFIOS_HOST/api/userCertificates")"
_list_code="$(_uos_response_code)"
if [ "$_list_code" != "200" ]; then
_err "Failed to list existing certificates (HTTP $_list_code)."
_err "Response: $_list_json"
return 1
fi
# _normalizeJson collapses the response to one predictable line (no stray
# whitespace around colons, no embedded CR/LF the server might emit) but
# also strips the trailing newline entirely -- re-terminate before the
# split below, since some sed implementations drop an unterminated final
# line rather than processing it.
_list_json="$(echo "$_list_json" | _normalizeJson)"
# A literal embedded newline (not the two-character "\n", which GNU sed
# treats as a newline in the replacement but POSIX doesn't define and BSD
# sed emits literally) splits it one JSON object per line so grep can
# match a single certificate entry at a time.
_list_json="$(
printf '%s\n' "$_list_json" | sed 's/},{/},\
{/g'
)"
_new_id="$(echo "$_list_json" | grep -F "\"fingerprint\":\"$_uos_fingerprint\"" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
if [ -z "$_new_id" ]; then
_err "Certificate upload rejected as a duplicate (server reported USER_CERTIFICATE_DUPLICATE), but no existing entry matching this fingerprint was found."
_err "Response: $_create_json"
return 1
fi
# Reusing the existing entry rather than deleting it and re-uploading
# under today's name+timestamp: the served content is identical either
# way, so replacing it would only cost an extra delete+create round trip
# for no functional benefit. The tradeoff is cosmetic -- this entry keeps
# whatever name it was given whenever it was originally uploaded, so it
# won't reflect today's date in the UI.
_info "Certificate already present as entry $_new_id; reusing it."
else
_err "Certificate upload failed (HTTP $_create_code)."
_err "Response: $_create_json"
return 1
fi
_info "Activating certificate $_new_id..."
_activate_json="$(_post '{"active":true}' "$DEPLOY_UNIFIOS_HOST/api/userCertificates/$_new_id/status" "" "PUT" "application/json")"
_activate_code="$(_uos_response_code)"
if [ "$_activate_code" != "200" ]; then
_err "Failed to activate new certificate (HTTP $_activate_code)."
_err "Response: $_activate_json"
return 1
fi
# UniFi OS Server activation is exclusive server-wide. Tests against the
# real API confirm this: activation of one entry deactivates whichever
# other entry was active before, no matter its name or domain. As a
# result, the server serves the certificate that this hook just activated.
# This certificate is already live. If the removal of old entries below
# fails, the hook logs the failure. The deploy does not fail because of
# this.
_info "Checking for old certificate entries to remove..."
_list_json="$(_get "$DEPLOY_UNIFIOS_HOST/api/userCertificates")"
_list_code="$(_uos_response_code)"
if [ "$_list_code" != "200" ]; then
_err "Failed to list certificates for cleanup (HTTP $_list_code) -- leaving old entries in place."
else
_list_json="$(echo "$_list_json" | _normalizeJson)"
_list_json="$(
printf '%s\n' "$_list_json" | sed 's/},{/},\
{/g'
)"
# The pattern below matches the domain name followed by a space. If the
# space is missing, the pattern can also match a different domain that
# starts with the same text as this domain.
_old_ids="$(echo "$_list_json" | grep -F "\"name\":\"$_cdomain " | _egrep_o '"id":"[^"]*"' | cut -d '"' -f 4 | grep -v "^$_new_id$")"
for _old_id in $_old_ids; do
_info "Removing old certificate entry $_old_id..."
_del_json="$(_post "" "$DEPLOY_UNIFIOS_HOST/api/userCertificates/$_old_id" "" "DELETE")"
_del_code="$(_uos_response_code)"
if [ "$_del_code" != "204" ] && [ "$_del_code" != "200" ]; then
_err "Failed to delete old certificate $_old_id (HTTP $_del_code) -- leaving it in place."
_err "Response: $_del_json"
fi
done
fi
_info "UniFi OS Server certificate deployed and activated successfully."
return 0
}

158
deploy/windows_rdp.sh Normal file
View file

@ -0,0 +1,158 @@
#!/usr/bin/env sh
# install a certificate on a Windows host over OpenSSH and bind it to the Remote
# Desktop listener (RDP-Tcp).
#
# One ssh invocation does the whole job:
# * the PFX is built locally, base64'd, and embedded as a string literal
# inside a generated PowerShell script;
# * the script is piped to `powershell.exe -Command -` over ssh. No scp,
# no temp files on the Windows host.
#
# First run:
# export DEPLOY_WIN_RDP_HOST=winserver.example.com
# acme.sh --deploy -d winserver.example.com --deploy-hook windows_rdp
#
# Available variables:
# DEPLOY_WIN_RDP_HOST required SSH host
# DEPLOY_WIN_RDP_USER optional SSH user, must be a local administrator (can also by set via ssh_config)
# DEPLOY_WIN_RDP_PORT optional SSH port, default 22
# DEPLOY_WIN_RDP_SSH_OPTS optional extra ssh options, e.g.
# "-i /root/.ssh/win_id_ed25519 -o StrictHostKeyChecking=yes"
# DEPLOY_WIN_RDP_LISTENER optional RDP listener name, default RDP-Tcp
# DEPLOY_WIN_RDP_RESTART optional "1" to restart TermService after install.
# Active RDP sessions will drop!
windows_rdp_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if ! _exists "ssh"; then
_err "ssh is required but was not found in PATH."
return 1
fi
# ---- configuration ------------------------------------------------------
_getdeployconf DEPLOY_WIN_RDP_HOST
_getdeployconf DEPLOY_WIN_RDP_USER
_getdeployconf DEPLOY_WIN_RDP_PORT
_getdeployconf DEPLOY_WIN_RDP_SSH_OPTS
_getdeployconf DEPLOY_WIN_RDP_LISTENER
_getdeployconf DEPLOY_WIN_RDP_RESTART
if [ -z "$DEPLOY_WIN_RDP_HOST" ]; then
_err "DEPLOY_WIN_RDP_HOST must be set."
return 1
fi
_savedeployconf DEPLOY_WIN_RDP_HOST "$DEPLOY_WIN_RDP_HOST"
[ -n "$DEPLOY_WIN_RDP_USER" ] && _savedeployconf DEPLOY_WIN_RDP_USER "$DEPLOY_WIN_RDP_USER"
[ -n "$DEPLOY_WIN_RDP_PORT" ] && _savedeployconf DEPLOY_WIN_RDP_PORT "$DEPLOY_WIN_RDP_PORT"
[ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ] && _savedeployconf DEPLOY_WIN_RDP_SSH_OPTS "$DEPLOY_WIN_RDP_SSH_OPTS"
[ -n "$DEPLOY_WIN_RDP_LISTENER" ] && _savedeployconf DEPLOY_WIN_RDP_LISTENER "$DEPLOY_WIN_RDP_LISTENER"
[ -n "$DEPLOY_WIN_RDP_RESTART" ] && _savedeployconf DEPLOY_WIN_RDP_RESTART "$DEPLOY_WIN_RDP_RESTART"
_port="${DEPLOY_WIN_RDP_PORT:-22}"
_listener="${DEPLOY_WIN_RDP_LISTENER:-RDP-Tcp}"
if [ -n "$DEPLOY_WIN_RDP_USER" ]; then
_target="$DEPLOY_WIN_RDP_USER@$DEPLOY_WIN_RDP_HOST"
else
_target="$DEPLOY_WIN_RDP_HOST"
fi
_pfx_pass="acme"
# ---- build thumbprint + PFX locally ------------------------------------
_thumb="$(_fingerprint "$_ccert" 'sha1')"
if [ -z "$_thumb" ]; then
_err "Failed to compute certificate thumbprint."
return 1
fi
_debug "Thumbprint: $_thumb"
_debug "Building PFX at $_pfx_file"
_pfx_file="$(_mktemp)"
if ! _toPkcs "$_pfx_file" "$_ckey" "$_ccert" "$_cca" "$_pfx_pass"; then
_err "Failed to build PFX archive."
rm -f "$_pfx_file"
return 1
fi
_pfx_b64=$(_base64 "multiline" <"$_pfx_file")
rm -f "$_pfx_file"
# ---- build installer script --------------------------------------------
if [ "$DEPLOY_WIN_RDP_RESTART" = "1" ]; then
_restart_ps='Restart-Service -Name TermService -Force'
else
_restart_ps='# New RdP connections will pick up the new cert automatically.'
fi
# Escape every literal `$` with `\$` so the shell does not expand it.
# Values substituted from shell: $_pfx_b64, $_pfx_pass, $_thumb, $_listener.
_ps1=$(
cat <<PSEOF
\$ErrorActionPreference = 'Stop'
\$pfxBytes = [Convert]::FromBase64String('${_pfx_b64}')
# Note: It is quite important to use a X509Certificate2Collection here in any case, since we otherwise
# could run into quite a lot of trouble when importing the certificate including its entire chain
# and its private key. Windows might behave arbitrarily and not consistently import the certificate
# at all - unless "Exportable" is included in the storage flags. However, then the certificate seems
# unaccessible to TermService for some weird reasons despite all permissions being set (at least on my
# Win 11 lab machine). This might be some security setting that prevents TermService from working with
# exportable keys? I don't know - importing the entire collection including chain or not always fixes
# the issues.
#
# Note2: If you should have kicked yourself out for some reason, then deleting the certificate will make
# TermService restore the original, self-signed certificate after at least after the second login attempt.
# Deleting the certificate can be easily accomplished via the Powershell, since SSH access will still be
# present in any case - the following command should get you out of trouble:
# \$cert = Get-ChildItem -Path 'Cert:\LocalMachine\My\\${_thumb}' | Select-Object -First 1 | Remove-Item
\$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]'MachineKeySet,PersistKeySet'
\$certs = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
\$certs.Import(\$pfxBytes, '${_pfx_pass}', \$flags)
\$store = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'LocalMachine')
\$store.Open('ReadWrite')
\$store.AddRange(\$certs)
\$store.Close()
Write-Host "Installed certs into LocalMachine\\My"
\$ts = Get-CimInstance -Namespace root/cimv2/terminalservices -ClassName Win32_TSGeneralSetting -Filter "TerminalName='${_listener}'"
if (-not \$ts) { throw "Listener '${_listener}' not found." }
Set-CimInstance -InputObject \$ts -Property @{SSLCertificateSHA1Hash="${_thumb}"}
Write-Host "Listener ${_listener} now uses ${_thumb}"
${_restart_ps}
PSEOF
)
_debug "Powershell script:${_ps1}"
# ---- run over a single ssh connection ----------------------------------
_ssh_opts="-o BatchMode=yes -p $_port"
if [ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ]; then
_ssh_opts="$_ssh_opts $DEPLOY_WIN_RDP_SSH_OPTS"
fi
_info "Deploying to $DEPLOY_WIN_RDP_HOST ..."
# shellcheck disable=SC2086
if ! printf '%s\n' "$_ps1" | ssh $_ssh_opts "$_target" \
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -'; then
_err "Remote install failed. Re-run acme.sh with --debug to see the PowerShell output."
return 1
fi
_info "Certificate for $_cdomain deployed and bound to $_listener on $DEPLOY_WIN_RDP_HOST."
return 0
}

View file

@ -7,6 +7,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_1984hosting
Options:
One984HOSTING_Username Username
One984HOSTING_Password Password
One984HOSTING_TOTP_Secret Base32 TOTP shared secret. Required only if the account has 2FA enabled. Requires oathtool. Used to mint the OTP code automatically at login so cron renewals keep working.
Issues: github.com/acmesh-official/acme.sh/issues/2851
Author: Adrian Fedoreanu
'
@ -124,11 +125,28 @@ _1984hosting_login() {
_debug "Login to 1984Hosting as user $One984HOSTING_Username."
username=$(printf '%s' "$One984HOSTING_Username" | _url_encode)
password=$(printf '%s' "$One984HOSTING_Password" | _url_encode)
url="https://1984.hosting/api/auth/"
_get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken"
csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
# When 2FA is enabled, mint a fresh TOTP code from the stored shared secret.
# Empty otpkey is accepted by the server when 2FA is off.
otpkey=""
if [ -n "$One984HOSTING_TOTP_Secret" ]; then
if ! _exists oathtool; then
_err "oathtool is required to use One984HOSTING_TOTP_Secret for 2FA. Please install it."
return 1
fi
otpcode="$(oathtool --base32 --totp "$One984HOSTING_TOTP_Secret" 2>/dev/null)"
if [ -z "$otpcode" ]; then
_err "Failed to generate TOTP code from One984HOSTING_TOTP_Secret."
return 1
fi
otpkey="$(printf '%s' "$otpcode" | _url_encode)"
fi
# Fetch the login page to obtain CSRF and session cookies.
# Note: _get sets the global 'url', so assign the auth URL afterwards.
_get "https://1984.hosting/accounts/login/" >/dev/null
csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')"
if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
_err "One or more cookies are empty: '$csrftoken', '$sessionid'."
@ -140,17 +158,23 @@ _1984hosting_login() {
csrf_header=$(echo "$csrftoken" | sed 's/csrftoken=//' | _head_n 1)
export _H3="X-CSRFToken: $csrf_header"
response="$(_post "username=$username&password=$password&otpkey=" $url)"
url="https://1984.hosting/api/auth/"
response="$(_post "username=$username&password=$password&otpkey=$otpkey" "$url")"
response="$(echo "$response" | _normalizeJson)"
_debug2 response "$response"
if _contains "$response" '"loggedin": true'; then
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')"
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
export One984HOSTING_SESSIONID_COOKIE
export One984HOSTING_CSRFTOKEN_COOKIE
_saveaccountconf_mutable One984HOSTING_Username "$One984HOSTING_Username"
_saveaccountconf_mutable One984HOSTING_Password "$One984HOSTING_Password"
if [ -n "$One984HOSTING_TOTP_Secret" ]; then
_saveaccountconf_mutable One984HOSTING_TOTP_Secret "$One984HOSTING_TOTP_Secret"
else
_clearaccountconf_mutable One984HOSTING_TOTP_Secret
fi
_saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE"
_saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE"
return 0
@ -161,6 +185,7 @@ _1984hosting_login() {
_check_credentials() {
One984HOSTING_Username="${One984HOSTING_Username:-$(_readaccountconf_mutable One984HOSTING_Username)}"
One984HOSTING_Password="${One984HOSTING_Password:-$(_readaccountconf_mutable One984HOSTING_Password)}"
One984HOSTING_TOTP_Secret="${One984HOSTING_TOTP_Secret:-$(_readaccountconf_mutable One984HOSTING_TOTP_Secret)}"
if [ -z "$One984HOSTING_Username" ] || [ -z "$One984HOSTING_Password" ]; then
One984HOSTING_Username=""
One984HOSTING_Password=""
@ -225,9 +250,15 @@ _get_root() {
# Usage: _get_zone_id url domain.com
# Returns zone id for domain.com
# Memoized per-domain so add/rm don't re-fetch the same zone list within a run.
# Keyed on domain (not url) since the url is always the domains listing.
_get_zone_id() {
url=$1
domain=$2
if [ "$_zone_id_for" = "$domain" ] && [ -n "$_zone_id" ]; then
_debug2 _zone_id "$_zone_id (cached)"
return 0
fi
_htmlget "$url" "$domain"
_zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+' | _head_n 1)"
_debug2 _zone_id "$_zone_id"
@ -235,6 +266,7 @@ _get_zone_id() {
_err "Error getting _zone_id for $2."
return 1
fi
_zone_id_for="$domain"
return 0
}
@ -257,9 +289,8 @@ _htmlget() {
# Add extra headers to request
_authpost() {
url="https://1984.hosting/domains"
_get_zone_id "$url" "$_domain"
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")"
_get_zone_id "https://1984.hosting/domains" "$_domain"
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | sed 's/csrftoken=//' | _head_n 1)"
export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE"
export _H2="Referer: https://1984.hosting/domains/$_zone_id"
export _H3="X-CSRFToken: $csrf_header"

View file

@ -37,6 +37,16 @@ dns_acmedns_add() {
ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readdomainconf ACMEDNS_PASSWORD)}"
ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readdomainconf ACMEDNS_SUBDOMAIN)}"
#for compatibility: old versions stored ACMEDNS_UPDATE_URL in the account
#conf (issue 3899). Do not clear it here: it must stay available for the
#other domains that have not migrated to their domain conf yet.
if [ -z "$ACMEDNS_BASE_URL" ]; then
_acmedns_update_url="$(_readaccountconf_mutable ACMEDNS_UPDATE_URL)"
if [ "$_acmedns_update_url" ]; then
ACMEDNS_BASE_URL="$(echo "$_acmedns_update_url" | sed 's#/update$##')"
fi
fi
if [ "$ACMEDNS_BASE_URL" = "" ]; then
ACMEDNS_BASE_URL="https://auth.acme-dns.io"
fi
@ -71,7 +81,7 @@ dns_acmedns_add() {
data="{\"subdomain\":\"$ACMEDNS_SUBDOMAIN\", \"txt\": \"$txtvalue\"}"
_debug data "$data"
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST")"
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST" "application/json")"
_debug response "$response"
if ! echo "$response" | grep "\"$txtvalue\"" >/dev/null; then

View file

@ -18,7 +18,9 @@ Ali_DNS_API="https://alidns.aliyuncs.com/"
#Usage: dns_ali_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_ali_add() {
fulldomain=$1
# the API only accepts punycode for IDN domains, and a raw UTF-8 domain
# also breaks the request signature (issue 4733)
fulldomain=$(_idn "$1")
txtvalue=$2
_prepare_ali_credentials || return 1
@ -33,7 +35,7 @@ dns_ali_add() {
}
dns_ali_rm() {
fulldomain=$1
fulldomain=$(_idn "$1")
txtvalue=$2
Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
@ -69,8 +71,8 @@ _ali_rest() {
ign="$2"
mtd="${3:-GET}"
signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _url_encode upper-hex)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
signature=$(printf "%s" "$signature" | _url_encode upper-hex)
signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _ali_urlencode_upper)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
signature=$(printf "%s" "$signature" | _ali_urlencode_upper)
url="$endpoint?Signature=$signature"
if [ "$mtd" = "GET" ]; then
@ -96,13 +98,28 @@ _ali_rest() {
fi
}
_ali_nonce() {
#_head_n 1 </dev/urandom | _digest "sha256" hex | cut -c 1-31
#Not so good...
date +"%s%N" | sed 's/%N//g'
# stdin stdout
# The Aliyun signature requires percent-encoding with upper-case hex.
# Do not use "_url_encode upper-hex" here: this file is also bundled by
# third parties (e.g. Proxmox VE proxmox-acme) whose older copies of the
# acme.sh function library ignore the upper-hex argument and output
# lower-case hex, which invalidates the signature.
# https://github.com/acmesh-official/acme.sh/issues/6272
_ali_urlencode_upper() {
{
_url_encode
echo
} | sed 's/%a/%A/g;s/%b/%B/g;s/%c/%C/g;s/%d/%D/g;s/%e/%E/g;s/%f/%F/g;s/%\(.\)a/%\1A/g;s/%\(.\)b/%\1B/g;s/%\(.\)c/%\1C/g;s/%\(.\)d/%\1D/g;s/%\(.\)e/%\1E/g;s/%\(.\)f/%\1F/g'
}
_timestamp() {
_ali_nonce() {
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
}
_ali_timestamp() {
date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ"
}
@ -150,7 +167,7 @@ _check_exist_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&TypeKeyWord=TXT'
query=$query'&Version=2015-01-09'
}
@ -166,7 +183,7 @@ _add_record_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Type=TXT'
query=$query'&Value='$3
query=$query'&Version=2015-01-09'
@ -182,7 +199,7 @@ _delete_record_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2015-01-09'
}
@ -196,7 +213,7 @@ _describe_records_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
query=$query'&Timestamp='$(_timestamp)
query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2015-01-09'
}

490
dnsapi/dns_arubabusiness.sh Normal file
View file

@ -0,0 +1,490 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_arubabusiness_info='ArubaBusiness
Site: business.aruba.it
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_arubabusiness
Options:
AB_Key Your ArubaBusiness API Key
AB_User Your account user
AB_Pass Your account password
'
#
# A word of warning: as of this writing, api.arubabusiness.it only supports oauth authentication using the "password" grant type.
# If you are REALLY sure you want to use it, it would be wise set up a dedicated technical user without administrative privileges
#
ARUBABUSINESS_API='https://api.arubabusiness.it'
######## Public functions ########
#
# Usage: dns_arubabusiness_add _acme-challenge.www.domain.com aaaabbbbcccc111122223333
#
# Add a new TXT record whose name and value match the given domain and value
#
# Variables
# _full_domain: $1 - the name of the TXT record
# _txt_value: $2 - the value of the TXT record
# _body
# dns_details
# domain_id
# dns_record_id
# response
#
dns_arubabusiness_add() {
_full_domain=$1
_txt_value=$2
if ! _ab_authenticate; then
return 1
fi
if ! _ab_domain_id "$_full_domain"; then
return 1
fi
if _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
# This is very unlikely, but allow the process to use the existing record
_info "A TXT record with name: $_full_domain and value: $_txt_value already exists (id: $dns_record_id)"
return 0
fi
_body="{ \"IdDomain\": $domain_id, \"Type\": \"TXT\", \"Name\": \"$_full_domain\", \"Content\": \"\\\"$_txt_value\\\"\" }"
_debug "Adding TXT record with name: $_full_domain and value: $_txt_value"
if ! _ab_rest POST "api/domains/dns/record" "$_body" || ! _contains "$response" "DomainId"; then
_err "Failed to add TXT record with name: $_full_domain"
return 1
fi
_info "Sleeping 10 seconds to let ArubaBusiness do its magic"
_sleep 10
# Refresh dns details and check that the record was really added
if ! _ab_dns_details "$root_domain"; then
return 1
fi
if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
# This should never happen
_err "The TXT record with name: $_full_domain was not set"
_err "Please check that the dns records are clean"
return 1
fi
_info "Added TXT record with id: $dns_record_id"
return 0
}
#
# Usage: dns_arubabusiness_rm _acme-challenge.www.domain.com aaaabbbbcccc111122223333
#
# Remove the TXT record whose name and value match the given domain and value
#
# Variables
# _full_domain: $1 - the name of the TXT record
# _txt_value: $2 - the value of the TXT record
# dns_details
# dns_record_id
#
dns_arubabusiness_rm() {
_full_domain=$1
_txt_value=$2
if ! _ab_authenticate; then
return 1
fi
if ! _ab_domain_id "$_full_domain"; then
return 1
fi
if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details" || [ -z "$dns_record_id" ]; then
_err "Could not retrieve the record id for: $_full_domain"
return 1
fi
_debug "Deleting TXT record: $dns_record_id"
if ! _ab_rest DELETE "api/domains/dns/record/$dns_record_id" || ! _contains "$response" "DomainId"; then
_err "Failed to delete TXT record: $dns_record_id"
return 1
fi
_info "Deleted TXT record: $dns_record_id"
return 0
}
######## Private functions ########
#
# Usage: _ab_domain_id _acme-challenge.www.domain.com
#
# Split the input domain into subdomain + root domain and get the id of the root domain
#
# Variables
# _full_domain: $1 - the domain whose root needs to be extracted
# _domain_sections
# _current_index
# _candidate_subdomain
# _candidate_domain
# sub_domain
# root_domain
# domain_id
# dns_details: a json containing all dns records registered on the root domain
#
# Example
# _get_root _acme-challenge.www.domain.com
#
# Should return
# sub_domain=_acme-challenge.www
# root_domain=domain.com
# domain_id=123123123123
# dns_details="{JSON_CONTENT}"
#
_ab_domain_id() {
_full_domain=$1
_info "Attempting to retrieve root domain details for: $_full_domain"
_domain_sections=$(_math "$(printf "%s" "$_full_domain" | tr '.' '\n' | wc -l)" + 1)
if [ "$_domain_sections" -lt 1 ]; then
_err "Invalid input $_full_domain"
return 1
fi
_current_index=1
while true; do
_candidate_subdomain=$(if [ "$_current_index" = "1" ]; then printf ""; else printf "%s" "$_full_domain" | cut -d . -f 1-"$(_math "$_current_index" - 1)"; fi)
_candidate_domain=$(printf "%s" "$_full_domain" | cut -d . -f "$_current_index"-"$_domain_sections")
if ! _ab_dns_details "$_candidate_domain"; then
_debug2 "Could not fetch dns details for: $_candidate_domain"
_current_index=$(_math "$_current_index" + 1)
# Fail if there are no candidates left
if [ "$_current_index" -gt "$_domain_sections" ]; then
_err "Could not determine the root domain for: $_full_domain"
return 1
fi
else
sub_domain="$_candidate_subdomain"
root_domain="$_candidate_domain"
# Extract the domain id, which is an integer and contains no commas
domain_id="$(printf "%s" "$dns_details" | _egrep_o '"Id":[^,]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')"
if [ -z "$domain_id" ]; then
_err "Could not determine the domain id for: $root_domain"
return 1
fi
_debug "Retrieved root domain id: $domain_id"
return 0
fi
done
}
#
# Usage: _ab_dns_record_id _acme-challenge.www.domain.com "aaaabbbbcccc111122223333" "{JSON_CONTENT}"
#
# Extract the record id of the first TXT record whose name and content match the input values
#
# Variables
# _record_name: $1
# _txt_value: $2
# _dns_details: $3 - the json returned by a previous call to '_ab_dns_details() $root_domain'
# _record_ids
# _record_names
# _record_types
# _record_contents
# _record_ids_count
# _record_names_count
# _record_types_count
# _record_contents_count
# _i
# dns_record_id
#
# Notes
# TXT correspond to record type 5
# ArubaBusiness appends a terminating dot (.) to the record name
# The content field may contain the following character sequence: \"
# All record names are always converted to lowercase
#
_ab_dns_record_id() {
_record_name=$1
_txt_value=$2
_dns_details=$3
_record_name_lowercase=$(printf "%s" "$_record_name" | _lower_case)
# Extract the record ids, which are integers and contain no commas, colons or spaces
# The first id is skipped because it refers to the domain id
_record_ids=$(printf "%s" "$_dns_details" | sed 's/"Id":/\n"Id":/g' | _egrep_o '"Id":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')
# Extract the record names, which are strings but cannot contain commas, colons, spaces and quotes
# The first name is skipped because it refers to the domain name
_record_names=$(printf "%s" "$_dns_details" | sed 's/"Name":/\n"Name":/g' | _egrep_o '"Name":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' "' | tr '\n' ' ')
# Extract the record types, which are integers (except for the first one) and contain no commas, colons or spaces
# The first type is skipped because it refers to the domain type
_record_types=$(printf "%s" "$_dns_details" | sed 's/"Type":/\n"Type":/g' | _egrep_o '"Type":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')
# Extract the record contents, which are strings and may contain no quotes except for TXT records, which must be delimited by two \" literals
# Note: There is no domain related entry here
# Note: A " character is appended at the end of each content to make it easier to process the list later
_record_contents=$(printf "%s" "$_dns_details" | sed 's/"Content":/\n"Content":/g' | sed 's/\\"//g' | _egrep_o '"Content": *"[^"]*"' | cut -d : -f 2- | sed -n 's/"\(.*\)"/\1/p' | tr '\n' '#')
_info "IDS: $_record_ids"
_info "NAMES: $_record_names"
_info "TYPEs: $_record_types"
_info "CONTENTS: $_record_contents"
_record_ids_count=$(printf "%s" "$_record_ids" | tr ' ' '\n' | wc -l)
_record_names_count=$(printf "%s" "$_record_names" | tr ' ' '\n' | wc -l)
_record_types_count=$(printf "%s" "$_record_types" | tr ' ' '\n' | wc -l)
_record_contents_count=$(printf "%s" "$_record_contents" | tr '#' '\n' | wc -l)
_info "Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
if [ "$_record_ids_count" != "$_record_names_count" ] || [ "$_record_ids_count" != "$_record_types_count" ] || [ "$_record_ids_count" != "$_record_contents_count" ]; then
_err "Failed to parse record elements. Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
return 1
fi
_info "Looking for a TXT record matching inputs - name: $_record_name_lowercase value: $_txt_value"
_i=1
while [ "$_i" -le "$_record_ids_count" ]; do
_current_name=$(printf "%s" "$_record_names" | cut -d " " -f "$_i")
_current_type=$(printf "%s" "$_record_types" | cut -d " " -f "$_i")
_current_content=$(printf "%s" "$_record_contents" | cut -d "#" -f "$_i")
if [ "$_record_name_lowercase." = "$_current_name" ] && [ "5" = "$_current_type" ] && [ "$_txt_value" = "$_current_content" ]; then
dns_record_id=$(printf "%s" "$_record_ids" | cut -d " " -f "$_i")
_info "Found matching record with id: $dns_record_id"
return 0
else
_debug2 "Record does not match - type: '$_current_type' name: '$_current_name' value: '$_current_content'; Expected '$_record_name_lowercase.' '5' '$_txt_value'"
fi
_i=$(_math "$_i" + 1)
done
_debug2 "No matching record was found in $_dns_details"
return 1
}
#
# Usage: _ab_dns_details domain.com
#
# Retrieve dns info for the given input domain
#
# Variables
# _domain: $1
# dns_details: the json returned by the call to $ARUBABUSINESS_API/api/domains/dns/$_domain/details (if return status is 0)
# response
#
_ab_dns_details() {
_domain=$1
if ! _ab_rest GET "api/domains/dns/$_domain/details" || ! _contains "$response" "DomainId"; then
return 1
fi
dns_details="$response"
return 0
}
#
# Usage: _ab_authenticate
#
# Read account conf, update domain conf and perform user authentication to acquire an access token
#
# Variables
# AB_Key
# AB_User
# AB_Pass
# AB_Token
#
_ab_authenticate() {
AB_Key="${AB_Key:-$(_readaccountconf_mutable AB_Key)}"
AB_User="${AB_User:-$(_readaccountconf_mutable AB_User)}"
AB_Pass="${AB_Pass:-$(_readaccountconf_mutable AB_Pass)}"
if [ -z "$AB_Key" ] || [ -z "$AB_User" ] || [ -z "$AB_Pass" ]; then
AB_Key=""
AB_User=""
AB_Pass=""
_err "Either the ArubaBusiness API key, the user or the password has not been defined yet."
_err "Please configure them and try again."
return 1
fi
_saveaccountconf_mutable AB_Key "$AB_Key"
_saveaccountconf_mutable AB_User "$AB_User"
_saveaccountconf_mutable AB_Pass "$AB_Pass"
if ! _ab_get_token || [ -z "$AB_Token" ]; then
_err "Failed to acquire an access token"
return 1
fi
return 0
}
#
# Usage: _ab_get_token
#
# Try acquiring a temporary access token. The token should have a 24h lifespan
#
# Variables
# _ab_user_enc
# _ab_pass_enc
# _ab_authdata
# AB_User
# AB_Pass
# AB_Token
# response
# _H2
#
_ab_get_token() {
_ab_user_enc=$(printf "%s" "$AB_User" | _url_encode)
_ab_pass_enc=$(printf "%s" "$AB_Pass" | _url_encode)
_ab_authdata="grant_type=password&username=$_ab_user_enc&password=$_ab_pass_enc"
_H2="Content-Type: application/x-www-form-urlencoded"
if ! _ab_rest POST "auth/token" "$_ab_authdata" || ! _contains "$response" "access_token"; then
_err "Authentication failure"
return 1
fi
AB_Token="$(printf "%s" "$response" | _egrep_o '"access_token":"[^\"]*"' | cut -d : -f 2 | tr -d '"')"
if [ -z "$AB_Token" ]; then
_err "Could not extract access token"
return 1
fi
_debug "Acquired access token"
return 0
}
#
# Usage: _ab_rest POST "example/endpoint" "password=123"
#
# Perform a REST request using the given method, endpoint and data
#
# Variables
# _method: $1 - The http method
# _endpoint: $2 - The api path (relative to $ARUBABUSINESS_API)
# _data: $3 - The body of the request (optional)
# _key_trimmed
# _token_trimmed
# _ret_code
# AB_Key
# AB_Token
# ARUBABUSINESS_API
# _H1
# _H2
# _H3
# _H4
#
_ab_rest() {
_method=$1
_endpoint="$2"
_data="$3"
_key_trimmed=$(printf "%s" "$AB_Key" | tr -d '"')
_token_trimmed=$(printf "%s" "$AB_Token" | tr -d '"')
_H1="Accept: application/json"
if [ -z "$_H2" ]; then
# Default to application/json
_H2="Content-Type: application/json"
fi
if [ "$_key_trimmed" ]; then
_H3="Authorization-Key: $_key_trimmed"
else
_err "Missing Api Key"
_ab_cleanup_headers
return 1
fi
if [ "$_token_trimmed" ]; then
_H4="Authorization: Bearer $_token_trimmed"
else
_debug "No access token set"
fi
if [ "$_method" != "GET" ]; then
response="$(_post "$_data" "$ARUBABUSINESS_API/$_endpoint" "" "$_method")"
else
response="$(_get "$ARUBABUSINESS_API/$_endpoint")"
fi
_ret_code=$?
if [ "$_ret_code" = "0" ] && _ab_call_is_success; then
# Normalize the json response
response="$(printf "%s" "$response" | _normalizeJson)"
_ret_code=0
else
_err "Failed to call endpoint: $_endpoint"
_ret_code=1
fi
_ab_cleanup_headers
return $_ret_code
}
#
# Usage: _ab_cleanup_headers
#
# Unset header variables to avoid interfering with other calls
#
# Variables
# _H1
# _H2
# _H3
# _H4
#
_ab_cleanup_headers() {
# Cleanup request headers
unset _H1 _H2 _H3 _H4 _H5
# Cleanup response headers
if [ -f "$HTTP_HEADER" ]; then
: >"$HTTP_HEADER"
fi
}
#
# Usage: _ab_call_is_success
#
# Check whether a call's response http status is one of 200, 201, 202 or 204 (other 2xx are not handled)
#
# Variables
# _status
# _http_status
# _success_http_codes
# HTTP_HEADER
#
_ab_call_is_success() {
_success_http_codes="200 201 202 204"
if [ -f "$HTTP_HEADER" ]; then
_http_status=$(_egrep_o "^HTTP[\/0-9. ]*" <"$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)
for _status in $_success_http_codes; do
if [ "$_status" = "$_http_status" ]; then
return 0
fi
done
fi
return 1
}

View file

@ -139,12 +139,21 @@ _get_autodns_zone() {
return 1
}
# Escape the XML special characters (& < > ' ") so that credentials
# containing them do not break the request document (issue 5317).
_autodns_xml_encode() {
sed "s/&/\&amp;/g;s/</\&lt;/g;s/>/\&gt;/g;s/'/\&apos;/g;s/\"/\&quot;/g"
}
_build_request_auth_xml() {
_autodns_user_xml="$(printf "%s" "$AUTODNS_USER" | _autodns_xml_encode)"
_autodns_password_xml="$(printf "%s" "$AUTODNS_PASSWORD" | _autodns_xml_encode)"
_autodns_context_xml="$(printf "%s" "$AUTODNS_CONTEXT" | _autodns_xml_encode)"
printf "<auth>
<user>%s</user>
<password>%s</password>
<context>%s</context>
</auth>" "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT"
</auth>" "$_autodns_user_xml" "$_autodns_password_xml" "$_autodns_context_xml"
}
# Arguments:

View file

@ -11,7 +11,8 @@ Options:
# 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"
# Updated from "route53.amazonaws.com"
AWS_HOST="route53.global.api.aws"
AWS_URL="https://$AWS_HOST"
AWS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Amazon-Route53-API"
@ -161,7 +162,7 @@ _get_root() {
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100 | sed 's/\./\\./g')
_debug "Checking domain: $h"
if [ -z "$h" ]; then
_error "invalid domain"
_err "invalid domain"
return 1
fi

775
dnsapi/dns_baidu.sh Normal file
View file

@ -0,0 +1,775 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
# Global variables for returning results (avoid stdout pollution from logging)
_BAIDU_FIND_RESULT=""
_BAIDU_BCE_AUTH_RESULT=""
: "${BAIDU_LOG_LEVEL:=2}"
_baidu_log_ts() {
date
}
_baidu_log_ge() {
_want="$1"
[ "${BAIDU_LOG_LEVEL:-0}" -ge "$_want" ]
}
_baidu_log() {
_lvl="$1"
_tag="$2"
_msg="$3"
if [ "$_lvl" = "0" ] || _baidu_log_ge "$_lvl"; then
printf -- "[%s] %s %s\n" "$(_baidu_log_ts)" "$_tag" "$_msg"
fi
}
_baidu_err() {
_baidu_log 0 "baidu_bcd.err" "$1"
return 1
}
_baidu_info() {
_baidu_log 1 "baidu_bcd.info" "$1"
return 0
}
_baidu_debug() {
_baidu_log 2 "$1" "$2"
return 0
}
dns_baidu_info='Baidu Cloud BCD DNS
Site: cloud.baidu.com
Docs: https://cloud.baidu.com/doc/BCD/
Signature: https://cloud.baidu.com/doc/Reference/s/njwvz1yfu
Options:
Baidu_AK AccessKeyId
Baidu_SK SecretAccessKey
OptionsAlt:
Baidu_BCD_Host API host, default: bcd.baidubce.com
Baidu_DNS_Host New DNS API host, default: dns.baidubce.com
Baidu_API_Preference Engine preference, default: auto
Baidu_BCD_Version API version number, default: 1
Baidu_BCD_Expire Signature expiration seconds, default: 3600
Baidu_View Resolve view, default: DEFAULT
Baidu_Line New DNS line, default: default
Baidu_TTL Resolve ttl seconds, default: 300
Baidu_RM_Max Max records to delete in one run, default: 20
'
BAIDU_BCD_DEFAULT_HOST="bcd.baidubce.com"
BAIDU_DNS_DEFAULT_HOST="dns.baidubce.com"
# --- Public API ---
dns_baidu_add() {
fulldomain=$(_idn "$1")
txtvalue=$2
if ! _baidu_run_with_fallback "add" "$fulldomain" "$txtvalue"; then
_baidu_err "all baidu api engines failed for add: $fulldomain"
return 1
fi
return 0
}
dns_baidu_rm() {
fulldomain=$(_idn "$1")
txtvalue=$2
if ! _baidu_run_with_fallback "rm" "$fulldomain" "$txtvalue"; then
_baidu_err "all baidu api engines failed for delete: $fulldomain"
return 1
fi
return 0
}
_baidu_run_with_fallback() {
_action="$1"
_fulldomain="$2"
_txtvalue="$3"
if ! _baidu_load_credentials; then
_baidu_err "baidu_load_credentials failed"
return 1
fi
for _baidu_api_engine in $(_baidu_engine_order); do
if ! _baidu_prepare_record "$_fulldomain"; then
_baidu_info "prepare failed for engine: $_baidu_api_engine"
continue
fi
if [ "$_action" = "add" ]; then
if _baidu_add_record "$_txtvalue"; then
return 0
fi
else
if _baidu_rm_record "$_txtvalue"; then
return 0
fi
fi
_baidu_info "engine failed, try next if available: $_baidu_api_engine"
done
return 1
}
_baidu_engine_order() {
_pref="$(_lower_case "$(_baidu_trim_ws "${Baidu_API_Preference:-auto}")")"
case "$_pref" in
legacy)
printf "%s" "legacy new"
;;
new)
printf "%s" "new legacy"
;;
*)
printf "%s" "new legacy"
;;
esac
}
_baidu_add_record() {
_txtvalue="$1"
if ! _baidu_find_record_ids_current "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then
_baidu_err "baidu_find_record_ids failed for add: $_record_domain.$_zone_name"
return 1
fi
_existing_ids="$_BAIDU_FIND_RESULT"
if [ "$_existing_ids" ]; then
_baidu_info "txt exists, skip add: $_record_domain.$_zone_name"
return 0
fi
_ttl="${Baidu_TTL:-300}"
_ttl="$(_baidu_trim_ws "$_ttl")"
case "$_ttl" in
"" | *[!0-9]*)
_ttl="300"
;;
esac
txtvalue="$(_baidu_trim_ws "$_txtvalue")"
_record_domain="$(_baidu_trim_ws "$_record_domain")"
_zone_name="$(_baidu_trim_ws "$_zone_name")"
if [ "$_baidu_api_engine" = "new" ]; then
_line="$(_baidu_trim_ws "${Baidu_Line:-default}")"
if [ -z "$_line" ]; then
_line="default"
fi
_body="$(_baidu_payload_add_txt_dns "$_record_domain" "$txtvalue" "$_ttl" "$_line")"
if ! _baidu_dns_call "POST" "/v1/dns/zone/${_zone_name}/record" "$_body"; then
_baidu_err "baidu_dns_call failed: add record"
return 1
fi
else
_view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")"
_body="$(_baidu_payload_add_txt "$_zone_name" "$_record_domain" "$txtvalue" "$_ttl" "$_view")"
if ! _baidu_bcd_post "/domain/resolve/add" "$_body"; then
_baidu_err "baidu_bcd_post failed: add record"
return 1
fi
fi
if _baidu_is_api_error "$response"; then
_baidu_err "$response"
return 1
fi
return 0
}
_baidu_rm_record() {
_txtvalue="$1"
if ! _baidu_find_record_ids_current "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then
_baidu_err "baidu_find_record_ids failed for delete: $_record_domain.$_zone_name"
return 1
fi
_ids="$_BAIDU_FIND_RESULT"
if [ -z "$_ids" ]; then
_baidu_info "no matching txt to delete: $_record_domain.$_zone_name"
return 0
fi
_rm_max="${Baidu_RM_Max:-20}"
_rm_max="$(_baidu_trim_ws "$_rm_max")"
case "$_rm_max" in
"" | *[!0-9]*)
_rm_max="20"
;;
esac
_rm_cnt="$(printf "%s\n" "$_ids" | sed '/^$/d' | wc -l | tr -d ' ')"
if [ "$_rm_cnt" ] && [ "$_rm_cnt" -gt "$_rm_max" ]; then
_baidu_err "Refusing to delete $_rm_cnt records (limit: $_rm_max)"
return 1
fi
for _rid in $_ids; do
if [ "$_baidu_api_engine" = "new" ]; then
if ! _baidu_dns_call "DELETE" "/v1/dns/zone/${_zone_name}/record/${_rid}" ""; then
_baidu_err "baidu_dns_call failed: delete recordId=$_rid"
return 1
fi
else
_body="$(_baidu_payload_delete "$_zone_name" "$_rid")"
if ! _baidu_bcd_post "/domain/resolve/delete" "$_body"; then
_baidu_err "baidu_bcd_post failed: delete recordId=$_rid"
return 1
fi
if _baidu_is_api_error "$response"; then
_baidu_err "$response"
return 1
fi
fi
done
if [ "$_baidu_api_engine" = "legacy" ]; then
if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then
_baidu_err "baidu_find_record_ids failed for delete verify: $_record_domain.$_zone_name"
return 1
fi
_left_ids="$_BAIDU_FIND_RESULT"
if [ -z "$_left_ids" ]; then
return 0
fi
if [ -n "$_left_ids" ]; then
_baidu_err "delete verification failed: $_record_domain.$_zone_name still has TXT records"
return 1
fi
fi
return 0
}
# --- Config / Record Context ---
_baidu_load_credentials() {
Baidu_AK="${Baidu_AK:-$(_readaccountconf_mutable Baidu_AK)}"
Baidu_SK="${Baidu_SK:-$(_readaccountconf_mutable Baidu_SK)}"
Baidu_AK="$(_baidu_trim_ws "$Baidu_AK")"
Baidu_SK="$(_baidu_trim_ws "$Baidu_SK")"
if [ -z "$Baidu_AK" ] || [ -z "$Baidu_SK" ]; then
_baidu_err "Baidu_AK and Baidu_SK are required"
return 1
fi
_saveaccountconf_mutable Baidu_AK "$Baidu_AK"
_saveaccountconf_mutable Baidu_SK "$Baidu_SK"
BAIDU_BCD_HOST="${Baidu_BCD_Host:-$BAIDU_BCD_DEFAULT_HOST}"
BAIDU_DNS_HOST="${Baidu_DNS_Host:-$BAIDU_DNS_DEFAULT_HOST}"
BAIDU_BCD_VERSION="${Baidu_BCD_Version:-1}"
return 0
}
_baidu_prepare_record() {
_fulldomain="$1"
if [ "$_baidu_api_engine" = "new" ]; then
if ! _baidu_get_root_dns "$_fulldomain"; then
_baidu_err "Could not find zone by new dns api for $_fulldomain"
return 1
fi
else
if ! _baidu_get_root "$_fulldomain"; then
_baidu_err "Could not find zone by legacy bcd api for $_fulldomain"
return 1
fi
fi
_record_domain="$_sub_domain"
_zone_name="$_domain"
return 0
}
# --- Zone / Records ---
_baidu_get_root() {
domain=$1
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
_baidu_err "invalid domain: $domain"
return 1
fi
if ! _baidu_bcd_post "/domain/resolve/list" "$(_baidu_payload_list "$h" 1 1)"; then
_baidu_err "baidu_bcd_post failed: list zones"
return 1
fi
if ! _baidu_is_api_error "$response" && (_contains "$response" "\"totalCount\"" || _contains "$response" "\"result\""); then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
if [ "$_sub_domain" = "$_domain" ]; then
_sub_domain="@"
fi
_baidu_info "zone matched: $_domain (host: $_sub_domain)"
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
}
_baidu_get_root_dns() {
domain=$1
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
_baidu_err "invalid domain: $domain"
return 1
fi
if ! _baidu_dns_call "GET" "/v1/dns/zone/${h}/record" ""; then
_baidu_info "baidu_dns_call failed: list zones"
elif ! _baidu_is_api_error "$response" && (_contains "$response" "\"records\"" || _contains "$response" "\"maxKeys\""); then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
if [ "$_sub_domain" = "$_domain" ]; then
_sub_domain="@"
fi
_baidu_info "zone matched by dns api: $_domain (host: $_sub_domain)"
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
}
_baidu_find_record_ids_current() {
if [ "$_baidu_api_engine" = "new" ]; then
_baidu_find_record_ids_dns "$@"
else
_baidu_find_record_ids "$@"
fi
}
_baidu_find_record_ids() {
_zone_name="$1"
_record_domain="$2"
_rdtype="$3"
_rdata="$4"
# Reset global result variable
_BAIDU_FIND_RESULT=""
_zone_name_e="$(_baidu_json_escape "$_zone_name")"
_record_domain_e="$(_baidu_json_escape "$_record_domain")"
_rdtype_e="$(_baidu_json_escape "$_rdtype")"
_rdata_e="$(_baidu_json_escape "$_rdata")"
_page=1
_page_size=100
_ids=""
_max_page=""
while true; do
if ! _baidu_bcd_post "/domain/resolve/list" "$(_baidu_payload_list "$_zone_name" "$_page" "$_page_size")"; then
_baidu_err "baidu_bcd_post failed: list records"
return 1
fi
if _baidu_is_api_error "$response"; then
_baidu_err "baidu_bcd error: $(_baidu_json_get_str "$response" "code") $(_baidu_json_get_str "$response" "message")"
return 1
fi
_normalized="$(
printf "%s" "$response" | _normalizeJson
)"
if [ -z "$_max_page" ]; then
_total="$(_baidu_parse_totalcount "$_normalized")"
_max_page="$(_baidu_calc_max_page "$_total" "$_page_size")"
fi
_records=$(printf "%s" "$_normalized" | sed 's/},{/}\n{/g')
while IFS= read -r _line; do
_id="$(_baidu_match_record_id "$_line" "$_record_domain_e" "$_rdtype_e" "$_rdata_e")"
if [ "$_id" ]; then
_ids="$_ids $_id"
fi
done <<EOF
$_records
EOF
if [ "$_page" -ge "$_max_page" ]; then
break
fi
_page=$(_math "$_page" + 1)
done
# Store result in global variable instead of stdout
_BAIDU_FIND_RESULT="$_ids"
}
_baidu_find_record_ids_dns() {
_zone_name="$1"
_record_domain="$2"
_rdtype="$3"
_rdata="$4"
_BAIDU_FIND_RESULT=""
if ! _baidu_dns_call "GET" "/v1/dns/zone/${_zone_name}/record" ""; then
_baidu_err "baidu_dns_call failed: list records"
return 1
fi
if _baidu_is_api_error "$response"; then
_baidu_err "baidu_dns error: $(_baidu_json_get_str "$response" "code") $(_baidu_json_get_str "$response" "message")"
return 1
fi
_normalized="$(printf "%s" "$response" | _normalizeJson)"
_records=$(printf "%s" "$_normalized" | sed 's/},{/}\n{/g')
_ids=""
while IFS= read -r _line; do
_id="$(_baidu_match_record_id_dns "$_line" "$_record_domain" "$_rdtype" "$_rdata")"
if [ "$_id" ]; then
_ids="$_ids $_id"
fi
done <<EOF
$_records
EOF
_BAIDU_FIND_RESULT="$_ids"
}
# --- HTTP ---
_baidu_bcd_post() {
_api_path="$1"
_payload="$2"
# BCD API requires JSON payload. Some call sites build fragments; normalize defensively.
_payload="$(_baidu_normalize_payload "$_payload")"
_ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
_expire="${Baidu_BCD_Expire:-3600}"
_content_type="application/json; charset=utf-8"
_payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)"
_uri="/v${BAIDU_BCD_VERSION}${_api_path}"
if ! _baidu_bce_auth "POST" "$_uri" "" "$BAIDU_BCD_HOST" "$_ts" "$_expire" "$_content_type" "$_payload_hash"; then
_baidu_err "baidu_bcd auth failed"
return 1
fi
_auth="$_BAIDU_BCE_AUTH_RESULT"
if [ -z "$_auth" ]; then
_baidu_err "baidu_bcd auth failed"
return 1
fi
_url="https://${BAIDU_BCD_HOST}${_uri}"
_signed_headers_dbg="$(printf "%s" "$_auth" | cut -d / -f 5)"
_baidu_info "POST ${_uri}"
_baidu_info "signedHeaders: $_signed_headers_dbg"
_baidu_info "payload_sha256: $_payload_hash"
_baidu_debug "baidu_bcd.http.payload" "$(_baidu_dbg_trim "$(_baidu_redact_txt "$_payload")")"
_H1="Authorization: $_auth"
_H2="x-bce-date: $_ts"
_H3="x-bce-content-sha256: $_payload_hash"
_H4="Host: $BAIDU_BCD_HOST"
_H5=""
response="$(_post "$_payload" "$_url" "" "POST" "$_content_type")"
_ret="$?"
_baidu_info "ret: $_ret"
_req_id="$(_baidu_json_get_str "$response" "requestId")"
_code="$(_baidu_json_get_str "$response" "code")"
_msg="$(_baidu_json_get_str "$response" "message")"
_baidu_info "response: requestId=${_req_id:-"-"} code=${_code:-"-"} message=$(_baidu_dbg_trim "${_msg:-"-"}")"
if [ "$_ret" != "0" ]; then
_baidu_err "baidu_bcd_post failed: $_uri"
return 1
fi
return 0
}
_baidu_dns_call() {
_method="$1"
_uri="$2"
_payload="$3"
_content_type="application/json"
_attempt=1
_max_attempts=3
while [ "$_attempt" -le "$_max_attempts" ]; do
_ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
_payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)"
if ! _baidu_bce_auth "$_method" "$_uri" "" "$BAIDU_DNS_HOST" "$_ts" "${Baidu_BCD_Expire:-3600}" "$_content_type" "$_payload_hash"; then
_baidu_err "baidu_dns auth failed"
return 1
fi
_auth="$_BAIDU_BCE_AUTH_RESULT"
_url="https://${BAIDU_DNS_HOST}${_uri}"
# Route through acme.sh's _get/_post (they honor _H1.._H5); no raw curl.
_H1="Authorization: $_auth"
_H2="x-bce-date: $_ts"
_H3="x-bce-content-sha256: $_payload_hash"
_H4="Host: $BAIDU_DNS_HOST"
_H5="Content-Type: $_content_type"
if [ "$_method" = "GET" ]; then
response="$(_get "$_url")"
elif [ "$_method" = "DELETE" ]; then
response="$(_post "" "$_url" "" "DELETE")"
else
response="$(_post "$_payload" "$_url")"
fi
_ret="$?"
_baidu_info "${_method} ${_uri} ret=${_ret}"
# Baidu may return a business error (Exception / 平台服务繁忙) inside HTTP 200.
if [ "$_ret" = "0" ] && ! _contains "$response" "\"code\":\"Exception\"" && ! _contains "$response" "平台服务繁忙"; then
return 0
fi
if [ "$_attempt" -lt "$_max_attempts" ]; then
sleep 2
fi
_attempt=$(_math "$_attempt" + 1)
done
return 1
}
# --- Auth / Signing ---
_baidu_bce_auth() {
# Signing algorithm (bce-auth-v1):
# - SigningKey = HMAC-SHA256-HEX(sk, authStringPrefix)
# - Signature = HMAC-SHA256-HEX(SigningKey, CanonicalRequest)
# Reference: https://cloud.baidu.com/doc/Reference/s/njwvz1yfu
_method="$1"
_uri="$2"
_query="$3"
_host="$4"
_ts="$5"
_expire="$6"
_ct="$7"
_payload_hash="$8"
_BAIDU_BCE_AUTH_RESULT=""
_auth_prefix="bce-auth-v1/${Baidu_AK}/${_ts}/${_expire}"
_signed_headers="content-type;host;x-bce-content-sha256;x-bce-date"
_canonical_uri="$(_baidu_bce_encode_path "$_uri")"
_canonical_query=""
_host_v="$(_baidu_trim_ws "$_host")"
_date_v="$(_baidu_trim_ws "$_ts")"
_ct_v="$(_baidu_trim_ws "$_ct")"
_host_e="$(printf "%s" "$_host_v" | _url_encode upper-hex)"
_date_e="$(printf "%s" "$_date_v" | _url_encode upper-hex)"
_ct_e="$(printf "%s" "$_ct_v" | _url_encode upper-hex)"
_hash_e="$(printf "%s" "$_payload_hash" | _url_encode upper-hex)"
_canonical_headers="content-type:${_ct_e}
host:${_host_e}
x-bce-content-sha256:${_hash_e}
x-bce-date:${_date_e}"
_canonical_request="${_method}
${_canonical_uri}
${_canonical_query}
${_canonical_headers}"
_sk_hex="$(printf "%s" "$Baidu_SK" | _hex_dump | tr -d " ")"
_signing_key="$(_baidu_hmac_sha256_hexkey "$_sk_hex" "$_auth_prefix")"
_signing_key_hex="$(printf "%s" "$_signing_key" | _hex_dump | tr -d " ")"
_signature="$(_baidu_hmac_sha256_hexkey "$_signing_key_hex" "$_canonical_request")"
_baidu_debug "baidu_bcd.auth" "bce_auth"
_baidu_debug "baidu_bcd.auth.auth_prefix" "bce-auth-v1/[ak]/${_ts}/${_expire}/$_signed_headers/[signature]"
_baidu_debug "baidu_bcd.auth.canonical_request_l" "$(printf "%s" "$_canonical_request" | sed -n 'l')"
_baidu_debug "baidu_bcd.auth.signature" "$(printf "%s" "$_signature" | cut -c 1-16)..."
_BAIDU_BCE_AUTH_RESULT="${_auth_prefix}/${_signed_headers}/${_signature}"
return 0
}
_baidu_bce_encode_path() {
_p="$1"
_out=""
if [ "${_p#"/"}" != "$_p" ]; then
_out="/"
fi
_rest="${_p#/}"
while [ -n "$_rest" ]; do
_seg="${_rest%%/*}"
if [ "$_seg" ]; then
if [ -z "$_out" ] || [ "$_out" = "/" ]; then
_out="${_out}$(printf "%s" "$_seg" | _url_encode upper-hex)"
else
_out="${_out}/$(printf "%s" "$_seg" | _url_encode upper-hex)"
fi
fi
if [ "${_rest#*/}" = "$_rest" ]; then
break
fi
_rest="${_rest#*/}"
done
if [ -z "$_out" ]; then
_out="/"
fi
printf "%s" "$_out"
}
# --- Utils ---
_baidu_trim() {
printf "%s" "$1" | sed 's/^ *//;s/ *$//'
}
_baidu_trim_ws() {
printf "%s" "$1" | tr '\r\n\t' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//'
}
_baidu_dbg_trim() {
printf "%s" "$1" | tr '\r\n' ' ' | cut -c 1-800
}
_baidu_is_api_error() {
_contains "$1" "\"code\"" && _contains "$1" "\"message\""
}
_baidu_normalize_payload() {
_p="$(_baidu_trim "$(printf "%s" "$1" | tr -d '\r')")"
if [ -z "$_p" ]; then
printf "%s" ""
return 0
fi
case "$_p" in
\{* | \[*)
printf "%s" "$_p"
;;
*)
printf "%s" "{$_p}"
;;
esac
}
_baidu_redact_txt() {
printf "%s" "$1" | sed 's/"rdata" *: *"[^"]*"/"rdata":"[redacted]"/g'
}
_baidu_json_get_str() {
_json="$1"
_key="$2"
printf "%s" "$_json" | _normalizeJson | sed -n "s/.*\"${_key}\" *: *\"\\([^\"]*\\)\".*/\\1/p" | _head_n 1
}
_baidu_json_escape() {
_s="$1"
_s="$(printf "%s" "$_s" | tr -d '\r\n')"
printf "%s" "$_s" |
sed 's/\\/\\\\/g; s/ /\\t/g' |
_baidu_json_encode
}
_baidu_json_encode() {
_j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")"
printf "%s" "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
}
_baidu_payload_list() {
_domain="$(_baidu_json_escape "$1")"
_pageNo="$2"
_pageSize="$3"
printf "%s" "{\"domain\":\"${_domain}\",\"pageNo\":${_pageNo},\"pageSize\":${_pageSize}}"
}
_baidu_payload_add_txt() {
_zoneName="$(_baidu_json_escape "$1")"
_domain="$(_baidu_json_escape "$2")"
_rdata="$(_baidu_json_escape "$3")"
_ttl="$4"
_view="$(_baidu_json_escape "$5")"
printf "%s" "{\"domain\":\"${_domain}\",\"view\":\"${_view}\",\"rdType\":\"TXT\",\"ttl\":${_ttl},\"rdata\":\"${_rdata}\",\"zoneName\":\"${_zoneName}\"}"
}
_baidu_payload_add_txt_dns() {
_rr="$(printf "%s" "$1" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
_value="$(printf "%s" "$2" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
_ttl="$3"
_line="$(printf "%s" "$4" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
printf "%s" "{\"rr\":\"${_rr}\",\"type\":\"TXT\",\"value\":\"${_value}\",\"ttl\":${_ttl},\"line\":\"${_line}\",\"description\":\"acme.sh\"}"
}
_baidu_payload_delete() {
_zoneName="$(_baidu_json_escape "$1")"
_recordId="$2"
printf "%s" "{\"zoneName\":\"${_zoneName}\",\"recordId\":${_recordId}}"
}
_baidu_parse_totalcount() {
_json="$1"
printf "%s" "$_json" | _egrep_o "\"totalCount\": *[0-9]*" | _head_n 1 | cut -d : -f 2 | tr -d " "
}
_baidu_calc_max_page() {
_total="$1"
_page_size="$2"
if [ -z "$_total" ]; then
printf "%s" "1"
return 0
fi
_max=$(((_total + _page_size - 1) / _page_size))
if [ "$_max" -lt 1 ]; then
_max=1
fi
printf "%s" "$_max"
}
_baidu_match_record_id() {
_line="$1"
_domain_e="$2"
_rdtype_e="$3"
_rdata_e="$4"
if ! _contains "$_line" "\"recordId\"" || (! _contains "$_line" "\"domain\":\"$_domain_e\"" && ! _contains "$_line" "\"domain\":\"${_domain_e}.\""); then
return 0
fi
if ! _contains "$_line" "\"rdtype\":\"$_rdtype_e\"" && ! _contains "$_line" "\"rdType\":\"$_rdtype_e\""; then
return 0
fi
if [ "$_rdata_e" ] && ! _contains "$_line" "\"rdata\":\"$_rdata_e\""; then
return 0
fi
printf "%s" "$_line" | _egrep_o "\"recordId\": *[0-9]*" | _head_n 1 | cut -d : -f 2 | tr -d " "
}
_baidu_match_record_id_dns() {
_line="$1"
_rr="$(printf "%s" "$2" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
_type="$(printf "%s" "$3" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
_value="$(printf "%s" "$4" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
case "$_line" in
*"\"rr\":\"${_rr}\""*"\"type\":\"${_type}\""*"\"value\":\"${_value}\""*)
printf "%s" "$_line" | sed -n 's/.*"id":"\{0,1\}\([^",}]*\)"\{0,1\}.*/\1/p' | _head_n 1
;;
esac
}
_baidu_hmac_sha256_hexkey() {
_key_hex="$1"
_msg="$2"
printf "%s" "$_msg" | _hmac sha256 "$_key_hex" hex
}

202
dnsapi/dns_bh.sh Executable file
View file

@ -0,0 +1,202 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_bh_info='Best-Hosting.cz
Site: best-hosting.cz
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bh
Options:
BH_API_USER API User identifier.
BH_API_KEY API Secret key.
Issues: github.com/acmesh-official/acme.sh/issues/6854
Author: @heximcz
'
BH_Api="https://best-hosting.cz/api/v1"
######## Public functions #####################
# Usage: dns_bh_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_bh_add() {
fulldomain=$1
txtvalue=$2
# --- 1. Credentials ---
BH_API_USER="${BH_API_USER:-$(_readaccountconf_mutable BH_API_USER)}"
BH_API_KEY="${BH_API_KEY:-$(_readaccountconf_mutable BH_API_KEY)}"
if [ -z "$BH_API_USER" ] || [ -z "$BH_API_KEY" ]; then
BH_API_USER=""
BH_API_KEY=""
_err "You must specify BH_API_USER and BH_API_KEY."
return 1
fi
_saveaccountconf_mutable BH_API_USER "$BH_API_USER"
_saveaccountconf_mutable BH_API_KEY "$BH_API_KEY"
# --- 2. Add TXT record ---
_info "Adding TXT record for $fulldomain"
json_payload="{\"fulldomain\":\"$fulldomain\",\"txtvalue\":\"$txtvalue\"}"
if ! _bh_rest POST "dns" "$json_payload"; then
_err "Failed to add DNS record."
return 1
fi
_norm_add=$(printf "%s" "$response" | tr -d '[:space:]')
if ! _contains "$_norm_add" '"status":"success"'; then
_err "API error: $response"
return 1
fi
record_id=$(printf "%s" "$_norm_add" | _egrep_o '"id":[0-9]+' | cut -d':' -f2)
_debug record_id "$record_id"
if [ -z "$record_id" ]; then
_err "Could not parse record ID from response."
return 1
fi
# Sanitize key — replace dots and hyphens with underscores
_conf_key=$(printf "%s" "BH_record_ids_${fulldomain}" | tr '.-' '_')
# Wildcard support: store space-separated list of IDs
# First call stores "111", second call stores "111 222"
_existing_ids=$(_readdomainconf "$_conf_key")
if [ -z "$_existing_ids" ]; then
_savedomainconf "$_conf_key" "$record_id"
else
_savedomainconf "$_conf_key" "$_existing_ids $record_id"
fi
_info "DNS TXT record added successfully."
return 0
}
# Usage: dns_bh_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_bh_rm() {
fulldomain=$1
txtvalue=$2
# --- 1. Credentials ---
BH_API_USER="${BH_API_USER:-$(_readaccountconf_mutable BH_API_USER)}"
BH_API_KEY="${BH_API_KEY:-$(_readaccountconf_mutable BH_API_KEY)}"
if [ -z "$BH_API_USER" ] || [ -z "$BH_API_KEY" ]; then
BH_API_USER=""
BH_API_KEY=""
_err "You must specify BH_API_USER and BH_API_KEY."
return 1
fi
# Sanitize key — same as in add
_conf_key=$(printf "%s" "BH_record_ids_${fulldomain}" | tr '.-' '_')
# --- 2. Load stored record ID(s) ---
_existing_ids=$(_readdomainconf "$_conf_key")
_debug _existing_ids "$_existing_ids"
if [ -z "$_existing_ids" ]; then
_err "Could not find record ID for $fulldomain."
return 1
fi
record_id=""
_remaining_ids=""
# Find the record ID that matches both the name and txtvalue
for _id in $_existing_ids; do
if ! _bh_rest GET "dns/$_id"; then
_debug "Failed to query record id $_id, skipping."
# Keep it in the list so a later run can try again
if [ -z "$_remaining_ids" ]; then
_remaining_ids="$_id"
else
_remaining_ids="$_remaining_ids $_id"
fi
continue
fi
_match_name=0
_match_content=0
_norm_response=$(printf "%s" "$response" | tr -d '[:space:]')
case "$_norm_response" in
*"\"name\":\"$fulldomain\""*)
_match_name=1
;;
esac
case "$_norm_response" in
*"\"content\":\"$txtvalue\""*)
_match_content=1
;;
esac
if [ "$_match_name" -eq 1 ] && [ "$_match_content" -eq 1 ]; then
record_id="$_id"
_debug "Matched record id" "$record_id"
# Do not add this ID to _remaining_ids; it will be deleted
continue
fi
# Not a match — keep ID for potential future cleanups
if [ -z "$_remaining_ids" ]; then
_remaining_ids="$_id"
else
_remaining_ids="$_remaining_ids $_id"
fi
done
if [ -z "$record_id" ]; then
_err "Could not find matching TXT record for $fulldomain with the given value."
return 1
fi
# --- 3. Delete record ---
_info "Removing TXT record for $fulldomain"
if ! _bh_rest DELETE "dns/$record_id"; then
_err "Failed to remove DNS record."
return 1
fi
# Update stored list — remove used ID
if [ -z "$_remaining_ids" ]; then
_cleardomainconf "$_conf_key"
else
_savedomainconf "$_conf_key" "$_remaining_ids"
fi
_info "DNS TXT record removed successfully."
return 0
}
#################### Private functions #####################
_bh_rest() {
m="$1"
ep="$2"
data="$3"
_debug "$ep"
_credentials="$(printf "%s:%s" "$BH_API_USER" "$BH_API_KEY" | _base64)"
export _H1="Authorization: Basic $_credentials"
export _H2="Content-Type: application/json"
export _H3="Accept: application/json"
if [ "$m" = "GET" ]; then
response="$(_get "$BH_Api/$ep")"
else
_debug data "$data"
response="$(_post "$data" "$BH_Api/$ep" "" "$m")"
fi
if [ "$?" != "0" ]; then
_err "Error calling $m $BH_Api/$ep"
return 1
fi
_debug2 response "$response"
return 0
}

373
dnsapi/dns_bhosted.sh Normal file
View file

@ -0,0 +1,373 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_bhosted_info='bHosted.nl DNS API
Site: bHosted.nl
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bhosted
Options:
BHOSTED_Username API username
BHOSTED_Password API password (MD5 hash like bHosted web services example)
BHOSTED_TTL TTL for TXT record (default: 300)
BHOSTED_SLD Optional override (useful for multi-part TLDs like co.uk)
BHOSTED_TLD Optional override (useful for multi-part TLDs like co.uk)
Notes:
- Plugin uses addrecord + delrecord for DNS-01 challenge
- Record ID is retrieved from addrecord XML response and cached for cleanup
'
BHOSTED_API_ROOT="https://webservices.bhosted.com/dns"
############ Public functions #####################
# Usage: dns_bhosted_add _acme-challenge.www.example.com "txt-value"
dns_bhosted_add() {
fulldomain="$1"
txtvalue="$2"
_debug "fulldomain" "$fulldomain"
_debug "txtvalue" "$txtvalue"
_bhosted_load_credentials || return 1
_bhosted_get_root "$fulldomain" || return 1
_info "Adding TXT record: ${_bhosted_name}.${_domain}"
BHOSTED_TTL="${BHOSTED_TTL:-$(_readaccountconf_mutable BHOSTED_TTL)}"
BHOSTED_TTL="${BHOSTED_TTL:-300}"
_saveaccountconf_mutable BHOSTED_TTL "$BHOSTED_TTL"
_bhosted_api_add_txt "$_bhosted_sld" "$_bhosted_tld" "$_bhosted_name" "$txtvalue" "$BHOSTED_TTL" || return 1
# Extract and cache record id in-memory for cleanup in this run
_rec_id="$(_bhosted_extract_id "$response")"
if [ -n "$_rec_id" ]; then
_hash="$(_bhosted_cache_hash "$fulldomain" "$txtvalue")"
_debug "_hash" "$_hash"
_debug "_rec_id" "$_rec_id"
_bhosted_mem_set_id "$_hash" "$_rec_id"
else
_err "TXT record added but no record id found in response."
_err "Cleanup may fail unless bHosted addrecord returns <id>...</id>."
_debug2 "add response" "$response"
return 1
fi
return 0
}
# Usage: dns_bhosted_rm _acme-challenge.www.example.com "txt-value"
dns_bhosted_rm() {
fulldomain="$1"
txtvalue="$2"
_debug "fulldomain" "$fulldomain"
_debug "txtvalue" "$txtvalue"
_bhosted_load_credentials || return 1
_bhosted_get_root "$fulldomain" || return 1
_hash="$(_bhosted_cache_hash "$fulldomain" "$txtvalue")"
_rec_id="$(_bhosted_mem_get_id "$_hash")"
if [ -z "$_rec_id" ]; then
_err "No cached bHosted record id found for cleanup."
_err "Please delete TXT manually in bHosted DNS for: ${_bhosted_name}.${_domain}"
return 1
fi
_info "Removing TXT record id=${_rec_id}: ${_bhosted_name}.${_domain}"
_bhosted_api_del_record "$_bhosted_sld" "$_bhosted_tld" "$_rec_id" || return 1
return 0
}
######## Private functions #####################
_bhosted_load_credentials() {
BHOSTED_Username="${BHOSTED_Username:-$(_readaccountconf_mutable BHOSTED_Username)}"
BHOSTED_Password="${BHOSTED_Password:-$(_readaccountconf_mutable BHOSTED_Password)}"
if [ -z "$BHOSTED_Username" ] || [ -z "$BHOSTED_Password" ]; then
BHOSTED_Username=""
BHOSTED_Password=""
_err "You didn't specify bHosted credentials."
_err "Please export BHOSTED_Username and BHOSTED_Password (MD5 hash)."
return 1
fi
_saveaccountconf_mutable BHOSTED_Username "$BHOSTED_Username"
_saveaccountconf_mutable BHOSTED_Password "$BHOSTED_Password"
return 0
}
# Determine root zone and host part
# Supports simple domains automatically (example.com, example.nl)
# For multi-part TLDs (example.co.uk), set:
# BHOSTED_SLD=example
# BHOSTED_TLD=co.uk
_bhosted_get_root() {
domain="$1"
BHOSTED_SLD="${BHOSTED_SLD:-$(_readdomainconf BHOSTED_SLD)}"
BHOSTED_TLD="${BHOSTED_TLD:-$(_readdomainconf BHOSTED_TLD)}"
if [ -n "$BHOSTED_SLD" ] && [ -n "$BHOSTED_TLD" ]; then
_savedomainconf BHOSTED_SLD "$BHOSTED_SLD"
_savedomainconf BHOSTED_TLD "$BHOSTED_TLD"
_domain="${BHOSTED_SLD}.${BHOSTED_TLD}"
case "$domain" in
*."$_domain") ;;
"$_domain") ;;
*)
_err "BHOSTED_SLD/BHOSTED_TLD do not match requested domain: $domain"
return 1
;;
esac
_bhosted_sld="$BHOSTED_SLD"
_bhosted_tld="$BHOSTED_TLD"
_bhosted_name="${domain%."$_domain"}"
if [ "$_bhosted_name" = "$domain" ]; then
_bhosted_name=""
fi
[ -n "$_bhosted_name" ] || _bhosted_name="@"
_debug "_domain" "$_domain"
_debug "_bhosted_sld" "$_bhosted_sld"
_debug "_bhosted_tld" "$_bhosted_tld"
_debug "_bhosted_name" "$_bhosted_name"
return 0
fi
# Auto-parse: assume last label = tld, label before = sld
# Works for .nl / .com / .org etc.
_bhosted_tld="$(printf "%s" "$domain" | awk -F. '{print $NF}')"
_bhosted_sld="$(printf "%s" "$domain" | awk -F. '{print $(NF-1)}')"
if [ -z "$_bhosted_sld" ] || [ -z "$_bhosted_tld" ]; then
_err "Could not parse SLD/TLD from domain: $domain"
return 1
fi
_domain="${_bhosted_sld}.${_bhosted_tld}"
_bhosted_name="${domain%."$_domain"}"
if [ "$_bhosted_name" = "$domain" ]; then
_bhosted_name=""
fi
[ -n "$_bhosted_name" ] || _bhosted_name="@"
_debug "_domain" "$_domain"
_debug "_bhosted_sld" "$_bhosted_sld"
_debug "_bhosted_tld" "$_bhosted_tld"
_debug "_bhosted_name" "$_bhosted_name"
return 0
}
_bhosted_api_add_txt() {
_sld="$1"
_tld="$2"
_name="$3"
_content="$4"
_ttl="$5"
_u_user="$(printf "%s" "$BHOSTED_Username" | _url_encode)"
_u_pass="$(printf "%s" "$BHOSTED_Password" | _url_encode)"
_u_sld="$(printf "%s" "$_sld" | _url_encode)"
_u_tld="$(printf "%s" "$_tld" | _url_encode)"
_u_name="$(printf "%s" "$_name" | _url_encode)"
_u_content="$(printf "%s" "$_content" | _url_encode)"
_u_ttl="$(printf "%s" "$_ttl" | _url_encode)"
_data="user=${_u_user}&password=${_u_pass}&tld=${_u_tld}&sld=${_u_sld}&type=TXT&name=${_u_name}&content=${_u_content}&ttl=${_u_ttl}"
_debug "bHosted add endpoint" "${BHOSTED_API_ROOT}/addrecord"
response="$(_post "$_data" "${BHOSTED_API_ROOT}/addrecord")"
_ret="$?"
_debug2 "bHosted add response" "$response"
if [ "$_ret" != "0" ]; then
_err "bHosted addrecord request failed"
return 1
fi
if _bhosted_response_has_error "$response"; then
_err "bHosted addrecord returned an error"
_debug2 "response" "$response"
return 1
fi
return 0
}
_bhosted_api_del_record() {
_sld="$1"
_tld="$2"
_id="$3"
_u_user="$(printf "%s" "$BHOSTED_Username" | _url_encode)"
_u_pass="$(printf "%s" "$BHOSTED_Password" | _url_encode)"
_u_sld="$(printf "%s" "$_sld" | _url_encode)"
_u_tld="$(printf "%s" "$_tld" | _url_encode)"
_u_id="$(printf "%s" "$_id" | _url_encode)"
_url="${BHOSTED_API_ROOT}/delrecord"
_data="user=${_u_user}&password=${_u_pass}&tld=${_u_tld}&sld=${_u_sld}&id=${_u_id}"
_debug "bHosted delete endpoint" "$_url"
response="$(_post "$_data" "$_url")"
_ret="$?"
_debug2 "bHosted delete response" "$response"
if [ "$_ret" != "0" ]; then
_err "bHosted delrecord request failed"
return 1
fi
if _bhosted_response_has_error "$response"; then
_err "bHosted delrecord returned an error"
_debug2 "response" "$response"
return 1
fi
return 0
}
# Extract XML tag value from response, e.g. <id>12345</id>
_bhosted_xml_value() {
_tag="$1"
_resp="$2"
# Flatten response to simplify parsing
_flat="$(printf "%s" "$_resp" | tr -d '\r\n\t')"
printf "%s" "$_flat" | sed -n "s:.*<${_tag}>\\([^<]*\\)</${_tag}>.*:\\1:p" | _head_n 1
}
# Return code convention:
# return 0 => response HAS error
# return 1 => response has NO error (success)
_bhosted_response_has_error() {
_resp="$1"
# Empty response = error
if [ -z "$_resp" ]; then
_debug "Empty API response"
return 0
fi
# Prefer explicit bHosted XML response fields
if _contains "$_resp" "<response>"; then
_errors="$(_bhosted_xml_value "errors" "$_resp")"
_done="$(_bhosted_xml_value "done" "$_resp")"
_subcommand="$(_bhosted_xml_value "subcommand" "$_resp")"
_id="$(_bhosted_xml_value "id" "$_resp")"
_debug "bHosted XML subcommand" "$_subcommand"
_debug "bHosted XML id" "$_id"
_debug "bHosted XML errors" "$_errors"
_debug "bHosted XML done" "$_done"
# Success according to provided format
if [ "$_errors" = "0" ] && [ "$_done" = "true" ]; then
return 1
fi
_debug "bHosted XML indicates failure"
return 0
fi
# Fallback for unexpected/non-XML responses
_resp_lc="$(_lower_case "$_resp")"
if _contains "$_resp_lc" "error"; then
_debug "Detected 'error' in response"
return 0
fi
if _contains "$_resp_lc" "fout"; then
_debug "Detected 'fout' in response"
return 0
fi
if _contains "$_resp_lc" "invalid"; then
_debug "Detected 'invalid' in response"
return 0
fi
if _contains "$_resp_lc" "failed"; then
_debug "Detected 'failed' in response"
return 0
fi
if _contains "$_resp_lc" "denied"; then
_debug "Detected 'denied' in response"
return 0
fi
# If no explicit error markers found, assume success
return 1
}
# Extract record id from response
# Supports bHosted XML first, then generic fallbacks
_bhosted_extract_id() {
_resp="$1"
# bHosted XML: <id>12345</id>
_id="$(_bhosted_xml_value "id" "$_resp" | tr -cd '0-9')"
if [ -n "$_id" ]; then
printf "%s" "$_id"
return 0
fi
# JSON: "id":12345
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[ ]*:[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
if [ -n "$_id" ]; then
printf "%s" "$_id"
return 0
fi
# key=value: id=12345
_id="$(printf "%s" "$_resp" | _egrep_o '(^|[^0-9a-zA-Z])id[ ]*=[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
if [ -n "$_id" ]; then
printf "%s" "$_id"
return 0
fi
# "record id 12345" / "recordid 12345"
_id="$(printf "%s" "$_resp" | _egrep_o '(record[ ]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
if [ -n "$_id" ]; then
printf "%s" "$_id"
return 0
fi
return 1
}
# Create a unique config key for cached record ids
_bhosted_cache_hash() {
_fd="$1"
_tv="$2"
# md5 hex of fulldomain|txtvalue
printf "%s|%s" "$_fd" "$_tv" | _digest md5 hex
}
_bhosted_cache_key() {
_hash="$1"
printf "%s" "BHOSTED_TXT_ID_${_hash}"
}
_bhosted_mem_set_id() {
_hash="$1"
_id="$2"
_key="$(_bhosted_cache_key "$_hash")"
_savedomainconf "$_key" "$_id"
}
_bhosted_mem_get_id() {
_hash="$1"
_key="$(_bhosted_cache_key "$_hash")"
_readdomainconf "$_key"
}

137
dnsapi/dns_calrissia.sh Normal file
View file

@ -0,0 +1,137 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_calrissia_info='Calrissia.be DNS API
Site: calrissia.be
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_calrissia
Options:
CALRISSIA_TOKEN Personal access token
Issues: github.com/acmesh-official/acme.sh/issues/6809
Author: Ward Hus
'
CALRISSIA_API="https://my.calrissia.com/api"
dns_calrissia_add() {
fulldomain="$1"
txtvalue="$2"
_calrissia_load_token || return 1
if ! _calrissia_get_root "$fulldomain"; then
_err "Unable to find domain in Calrissia account for: $fulldomain"
return 1
fi
_debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'"
_info "Adding TXT record for $fulldomain"
_body="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120,\"prio\":0}"
_response="$(_calrissia_request POST "/domain/$_domain_id/record" "$_body")"
if ! _contains "$_response" '"id"'; then
_err "Failed to create TXT record: $_response"
return 1
fi
return 0
}
dns_calrissia_rm() {
fulldomain="$1"
txtvalue="$2"
_calrissia_load_token || return 1
if ! _calrissia_get_root "$fulldomain"; then
_err "Unable to find domain in Calrissia account for: $fulldomain"
return 1
fi
_debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'"
# Look the record up from the API instead of relying on local state.
# The record list is embedded in the domain object.
_response="$(_calrissia_request GET "/domain/$_domain_id")"
_debug2 "Response: $_response"
# Split the record objects onto separate lines, then match on both the
# subdomain name and the TXT value to find the record id to delete.
_record_id="$(printf "%s" "$_response" |
tr '{}' '\n' |
grep "\"name\" *: *\"$_sub_domain\"" |
grep "\"content\" *: *\"$txtvalue\"" |
_egrep_o '"id" *: *[0-9]+' |
_head_n 1 |
_egrep_o '[0-9]+')"
if [ -z "$_record_id" ]; then
_info "No matching TXT record found for $fulldomain; nothing to remove"
return 0
fi
_info "Removing TXT record id=$_record_id from domain id=$_domain_id"
if ! _response="$(_calrissia_request DELETE "/domain/$_domain_id/record/$_record_id")" || _contains "$_response" '"error"'; then
_err "Failed to remove TXT record: $_response"
return 1
fi
return 0
}
####################
# Private helpers #
####################
_calrissia_load_token() {
CALRISSIA_TOKEN="${CALRISSIA_TOKEN:-$(_readaccountconf_mutable CALRISSIA_TOKEN)}"
if [ -z "$CALRISSIA_TOKEN" ]; then
_err "CALRISSIA_TOKEN is not set. Generate one at https://identity.calrissia.com under API Keys."
return 1
fi
_saveaccountconf_mutable CALRISSIA_TOKEN "$CALRISSIA_TOKEN"
}
# Sets _domain, _domain_id, _sub_domain for a given FQDN.
_calrissia_get_root() {
_fqdn="$1"
i=1
while true; do
_candidate="$(printf "%s" "$_fqdn" | cut -d . -f "$i"-)"
[ -z "$_candidate" ] && return 1
_debug "Trying root domain: $_candidate"
_response="$(_calrissia_request GET "/domain?full_domain_name=$_candidate")"
_debug2 "Response: $_response"
_domain_id="$(printf "%s" "$_response" |
_egrep_o '"id" *: *[0-9]+' |
_head_n 1 |
_egrep_o '[0-9]+')"
if [ -n "$_domain_id" ]; then
if [ "$i" = "1" ]; then
# The FQDN itself is the zone apex, e.g. a challenge-alias domain.
_sub_domain=""
else
_sub_domain="$(printf "%s" "$_fqdn" | cut -d . -f "1-$((i - 1))")"
fi
_domain="$_candidate"
return 0
fi
i=$((i + 1))
done
}
_calrissia_request() {
_method="$1"
_path="$2"
_body="$3"
export _H1="Authorization: Bearer $CALRISSIA_TOKEN"
export _H2="Accept: application/json"
if [ "$_method" = "GET" ]; then
_get "$CALRISSIA_API$_path"
else
_post "$_body" "$CALRISSIA_API$_path" "" "$_method" "application/json"
fi
}

137
dnsapi/dns_cdmon.sh Normal file
View file

@ -0,0 +1,137 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_cdmon_info='cdmon
Site: www.cdmon.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cdmon
Options:
CDMON_Key API Key
'
CDMON_Api="https://api-domains.cdmon.services/api-domains"
######## Public functions #####################
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
dns_cdmon_add() {
fulldomain=$1
txtvalue=$2
CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
if [ -z "$CDMON_Key" ]; then
CDMON_Key=""
_err "You didn't specify your cdmon api key yet."
_err "Please create your key and try again."
return 1
fi
_saveaccountconf_mutable CDMON_Key "$CDMON_Key"
_debug "First, we detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_info "Adding record"
if _cdmon_rest "dnsrecords/create" "{\"data\":{\"type\":\"TXT\",\"domain\":\"$_domain\",\"value\":\"$txtvalue\",\"ttl\":120,\"host\":\"$_sub_domain\"}}"; then
if _contains "$response" "\"status\":\"ok\""; then
_info "Added, OK"
return 0
else
_err "Add txt record error."
return 1
fi
fi
_err "Add txt record error."
return 1
}
# Usage: fulldomain txtvalue
# Used to remove the txt record after validation
dns_cdmon_rm() {
fulldomain=$1
txtvalue=$2
CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
_debug "First, we detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_info "Removing record"
if _cdmon_rest "dnsrecords/delete" "{\"data\":{\"value\":\"$txtvalue\",\"type\":\"TXT\",\"domain\":\"$_domain\",\"host\":\"$_sub_domain\"}}"; then
if _contains "$response" "\"status\":\"ok\""; then
_info "Deleted, OK"
return 0
else
_err "Delete txt record error."
return 1
fi
fi
_err "Delete txt record error."
return 1
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
_get_root() {
domain=$1
i=1
p=1
if ! _cdmon_rest "domains/list"; then
return 1
fi
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
#not valid
return 1
fi
if _contains "$response" "\"domain\":\"$h\""; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
_cdmon_rest() {
ep="$1"
data="$2"
_debug "$ep"
key_trimmed=$(echo "$CDMON_Key" | tr -d '"')
export _H1="Content-Type: application/json"
export _H2="apikey: $key_trimmed"
_debug data "$data"
response="$(_post "$data" "$CDMON_Api/$ep")"
_ret="$?"
unset _H1 _H2
if [ "$_ret" != "0" ]; then
_err "error $ep"
return 1
fi
_debug2 response "$response"
return 0
}

View file

@ -135,7 +135,7 @@ _dns_cloudns_init_check() {
_dns_cloudns_http_api_call "dns/login.json" ""
if ! _contains "$response" "\"status\":\"Success\""; then
_err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Please check your login credentials."
_err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Server response: $response"
return 1
fi

View file

@ -15,7 +15,8 @@ CN_API="https://beta.api.core-networks.de"
######## Public functions #####################
dns_cn_add() {
fulldomain=$1
# Core-Networks API requires punycode for IDN domains
fulldomain=$(_idn "$1")
txtvalue=$2
if ! _cn_login; then
@ -58,7 +59,8 @@ dns_cn_add() {
}
dns_cn_rm() {
fulldomain=$1
# Core-Networks API requires punycode for IDN domains
fulldomain=$(_idn "$1")
txtvalue=$2
if ! _cn_login; then

248
dnsapi/dns_comlaude.sh Normal file
View file

@ -0,0 +1,248 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_comlaude_info='comlaude.com
Site: comlaude.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_comlaude
Options:
COMLAUDE_USERNAME User account
COMLAUDE_PASSWORD User password
COMLAUDE_API_KEY generated API key
COMLAUDE_GROUP_ID Group ID in comlaude user profile
Get it from the https://www.comlaude.com
Issues: github.com/acmesh-official/acme.sh/issues/7112
'
# ===== CONFIG =====
COMLAUDE_API="https://api.comlaude.com"
########## AUTH ##########
_comlaude_auth() {
_debug "Checking cached ComLaude token"
# Try to get token from account.conf
if [ -z "$COMLAUDE_ACCESS_TOKEN" ]; then
COMLAUDE_ACCESS_TOKEN="$(_readaccountconf_mutable COMLAUDE_ACCESS_TOKEN)"
COMLAUDE_TOKEN_EXPIRY="$(_readaccountconf_mutable COMLAUDE_TOKEN_EXPIRY)"
fi
_now=$(_time)
if [ -n "$COMLAUDE_ACCESS_TOKEN" ] && [ -n "$COMLAUDE_TOKEN_EXPIRY" ] && [ "$_now" -lt "$COMLAUDE_TOKEN_EXPIRY" ]; then
_debug "Using cached ComLaude token (valid ${COMLAUDE_TOKEN_EXPIRY} > ${_now})"
return 0
fi
_info "ComLaude auth..."
_comlaude_body="{\"username\":\"$COMLAUDE_USERNAME\",\"password\":\"$COMLAUDE_PASSWORD\",\"api_key\":\"$COMLAUDE_API_KEY\"}"
_comlaude_response="$(_post "$_comlaude_body" "$COMLAUDE_API/api_login" "" "POST" "application/json")"
if ! _contains "$_comlaude_response" "access_token"; then
_err "Auth failed: $_comlaude_response"
return 1
fi
COMLAUDE_ACCESS_TOKEN=$(echo "$_comlaude_response" | _egrep_o '"access_token":"[^"]*"' | cut -d'"' -f4)
# store expiracy from api reply l'API ("expires_in" in seconds)
_comlaude_expires_in=$(echo "$_comlaude_response" | _egrep_o '"expires_in":[0-9]*' | cut -d: -f2)
[ -z "$_comlaude_expires_in" ] && _comlaude_expires_in=3000 # fallback if no info
COMLAUDE_TOKEN_EXPIRY=$(($(_time) + _comlaude_expires_in - 60)) # margin of 60s to secure renew
_saveaccountconf_mutable COMLAUDE_ACCESS_TOKEN "$COMLAUDE_ACCESS_TOKEN"
_saveaccountconf_mutable COMLAUDE_TOKEN_EXPIRY "$COMLAUDE_TOKEN_EXPIRY"
return 0
}
########## DOMAIN RESOLUTION ##########
_comlaude_get_root() {
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
if [ -z "$COMLAUDE_GROUP_ID" ]; then
_err "Missing COMLAUDE_GROUP_ID"
return 1
fi
_comlaude_input_domain="$1"
_comlaude_input_domain="${_comlaude_input_domain#_acme-challenge.}"
case "$_comlaude_input_domain" in
\*.*) _comlaude_input_domain="${_comlaude_input_domain#*.}" ;;
esac
_debug "Normalized domain: $_comlaude_input_domain"
_comlaude_i=1
while true; do
_comlaude_d=$(printf "%s" "$_comlaude_input_domain" | cut -d . -f "$_comlaude_i-")
[ -z "$_comlaude_d" ] && {
_debug "No matching domain found for $_comlaude_input_domain"
return 1
}
# don't test unnecessary levels
# registered domain : TLD only (no dot after cut).
case "$_comlaude_d" in
*.*) : ;;
*)
_debug "Skipping bare TLD candidate: $_comlaude_d"
_comlaude_i=$((_comlaude_i + 1))
continue
;;
esac
_debug "Checking domain: $_comlaude_d"
_comlaude_retry=0
_comlaude_max_retry=3 # to avoid network errors
_comlaude_DOM_ID=""
_comlaude_Z_ID=""
while [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ]; do
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
_debug "Full URL: $COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone"
_comlaude_response="$(_get "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone")"
_H1=""
_debug "RAW response for $_comlaude_d (try $((_comlaude_retry + 1))): $_comlaude_response"
# If empty -> true network issue, we retry
if [ -z "$_comlaude_response" ]; then
_comlaude_retry=$((_comlaude_retry + 1))
[ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2
continue
fi
# 404 -> domain not found in that level. no retry : continue
if echo "$_comlaude_response" | grep -q '"status_code":404'; then
_debug "404 for $_comlaude_d, moving to next level (not retrying)"
break
fi
# Domain missing (200 reply, data empty) -> continue
if echo "$_comlaude_response" | grep -q '"data":\[\]'; then
_debug "Empty data for $_comlaude_d, moving to next level"
break
fi
# Extraction via _egrep_o
_comlaude_DOM_ID="$(echo "$_comlaude_response" | _egrep_o '"id":"[^"]*"' | head -n1 | cut -d':' -f2 | tr -d '"')"
_comlaude_Z_ID="$(echo "$_comlaude_response" | _egrep_o '"active_zone":\{"id":"[^"]*"' | _egrep_o '"id":"[^"]*"$' | cut -d':' -f2 | tr -d '"')"
if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then
break
fi
# 200 reply but malformed data / noid -> retry transport
_comlaude_retry=$((_comlaude_retry + 1))
[ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2
done
_debug "_comlaude_DOM_ID=$_comlaude_DOM_ID"
_debug "_comlaude_Z_ID=$_comlaude_Z_ID"
if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then
_comlaude_domain="$_comlaude_d"
_comlaude_domain_id="$_comlaude_DOM_ID"
_comlaude_zone_id="$_comlaude_Z_ID"
return 0
fi
_comlaude_i=$((_comlaude_i + 1))
done
}
########## ADD TXT ##########
dns_comlaude_add() {
fulldomain="$1"
txtvalue="$2"
COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}"
COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}"
COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}"
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
if [ -z "$COMLAUDE_USERNAME" ] || [ -z "$COMLAUDE_PASSWORD" ] || [ -z "$COMLAUDE_API_KEY" ]; then
_err "You didn't specify ComLaude credentials (COMLAUDE_USERNAME, COMLAUDE_PASSWORD, COMLAUDE_API_KEY)."
return 1
fi
# Backup variable after validation
_saveaccountconf_mutable COMLAUDE_USERNAME "$COMLAUDE_USERNAME"
_saveaccountconf_mutable COMLAUDE_PASSWORD "$COMLAUDE_PASSWORD"
_saveaccountconf_mutable COMLAUDE_API_KEY "$COMLAUDE_API_KEY"
_saveaccountconf_mutable COMLAUDE_GROUP_ID "$COMLAUDE_GROUP_ID"
_info "Adding TXT: $fulldomain"
_comlaude_auth || return 1
_comlaude_get_root "$fulldomain" || return 1
_debug "Root: $_comlaude_domain"
_comlaude_data="{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"value\":\"$txtvalue\",\"ttl\":60}"
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
export _H2="Content-Type: application/json"
_comlaude_response="$(_post "$_comlaude_data" "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records")"
_H1=""
_H2=""
if ! echo "$_comlaude_response" | grep -q '"id"'; then
_err "Failed to create TXT"
_debug "$_comlaude_response"
return 1
fi
return 0
}
########## REMOVE TXT ##########
dns_comlaude_rm() {
fulldomain="$1"
txtvalue="$2"
COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}"
COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}"
COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}"
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
_info "Removing TXT: $fulldomain"
_comlaude_auth || return 1
_comlaude_get_root "$fulldomain" || return 1
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
_comlaude_encoded_name="$(printf '%s' "$fulldomain" | _url_encode)"
_comlaude_encoded_value="$(printf '%s' "$txtvalue" | _url_encode)"
_comlaude_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records?filter[type]=TXT&filter[name]=$_comlaude_encoded_name&filter[value]=$_comlaude_encoded_value"
_comlaude_response="$(_get "$_comlaude_url")"
_H1=""
_debug "Filtered records response: $_comlaude_response"
# first "id" top-level of reply (record itself,
# always on first position of each data[] object)
_comlaude_record_id="$(echo "$_comlaude_response" | _egrep_o '"data":\[\{"id":"[^"]*"' | _egrep_o '"[^"]*"$' | tr -d '"')"
if [ -z "$_comlaude_record_id" ]; then
_info "No matching TXT record found to delete for $fulldomain / $txtvalue"
return 0
fi
_debug "Deleting record $_comlaude_record_id"
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
_comlaude_del_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records/$_comlaude_record_id"
_comlaude_del_resp="$(_post "" "$_comlaude_del_url" "" "DELETE")"
_H1=""
if echo "$_comlaude_del_resp" | grep -q '"error"'; then
_err "Delete failed for $_comlaude_record_id"
_debug "$_comlaude_del_resp"
return 1
fi
_info "Deleted record $_comlaude_record_id"
return 0
}

View file

@ -38,7 +38,7 @@ dns_cpanel_add() {
fi
# adding entry
_info "Adding the entry"
stripped_fulldomain=$(echo "$fulldomain" | sed "s/.$_domain//")
stripped_fulldomain="${fulldomain%."$_domain"}"
_debug "Adding $stripped_fulldomain to $_domain zone"
_myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=add_zone_record&domain=$_domain&name=$stripped_fulldomain&type=TXT&txtdata=$txtvalue&ttl=1"
if _successful_update; then return 0; fi
@ -128,13 +128,27 @@ _get_root() {
_err "Primary domain list not found!"
return 1
fi
for _domain in $_domains; do
_debug "Checking if $fulldomain ends with $_domain"
if (_endswith "$fulldomain" "$_domain"); then
_debug "Root domain: $_domain"
return 0
fi
# Pick the LONGEST matching zone, dot-anchored: with both domain.tld and
# sub.domain.tld zones on the account, cPanel stores the record in the
# most specific zone, so add and rm must both resolve to that one.
_domain=""
for d in $_domains; do
_debug "Checking if $fulldomain ends with $d"
# case with quoted patterns gives an exact literal suffix match;
# _endswith treats the needle as a regex, so its dots would let
# xdomain.tld wrongly match zone domain.tld
case "$fulldomain" in
"$d" | *".$d")
if [ "${#d}" -gt "${#_domain}" ]; then
_domain="$d"
fi
;;
esac
done
if [ -n "$_domain" ]; then
_debug "Root domain: $_domain"
return 0
fi
return 1
}

269
dnsapi/dns_cpanel_uapi.sh Executable file
View file

@ -0,0 +1,269 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_cpanel_uapi_info='cPanel UAPI
Manage DNS via cPanel UAPI. Works with API tokens and Two-Factor Authentication.
Site: cpanel.net
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_cpanel_uapi
Options:
cPanel_Username Username
cPanel_Apitoken API Token
cPanel_Hostname Server URL. E.g. "https://hostname:port"
cPanel_TTL optional TXT record TTL in seconds. Default: 120
Issues: github.com/acmesh-official/acme.sh/issues/6877
Author: Adam Bodnar
'
######## Public functions #####################
# Used to add txt record
dns_cpanel_uapi_add() {
fulldomain=$1
txtvalue=$2
_info "Adding TXT record via cPanel UAPI"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _cpanel_uapi_get_root; then
_err "No matching root domain for $fulldomain found"
return 1
fi
# Build the record name relative to the zone
_escaped_domain=$(echo "$_domain" | sed 's/\./\\./g')
_record_name=$(echo "$fulldomain" | sed "s/\.${_escaped_domain}$//")
_debug "Record name: $_record_name in zone $_domain"
# Get the current SOA serial (required by mass_edit_zone)
if ! _cpanel_uapi_get_serial "$_domain"; then
_err "Failed to get zone serial for $_domain"
return 1
fi
_debug "Zone serial: $_serial"
# Use configurable TTL, default 120 seconds
_ttl="${cPanel_TTL:-$(_readaccountconf_mutable cPanel_TTL)}"
case "$_ttl" in
"")
_ttl=120
;;
*[!0-9]*)
_debug "Invalid cPanel_TTL provided, falling back to default 120"
_ttl=120
;;
esac
# Build JSON and URL-encode it for the add parameter
_add_json=$(printf '{"dname":"%s","ttl":%s,"record_type":"TXT","data":["%s"]}' "$_record_name" "$_ttl" "$txtvalue")
_debug "add_json: $_add_json"
_add_json_encoded=$(printf '%s' "$_add_json" | _url_encode)
_debug "add_json (encoded): $_add_json_encoded"
if ! _cpanel_uapi_request "execute/DNS/mass_edit_zone?zone=${_domain}&serial=${_serial}&add=${_add_json_encoded}"; then
_err "Request to add TXT record failed for zone $_domain"
return 1
fi
_debug "_result: $_result"
if _contains "$_result" '"status":1'; then
_info "TXT record added successfully"
return 0
fi
_err "Failed to add TXT record."
_err "Response: $_result"
return 1
}
# Used to remove the txt record after validation
dns_cpanel_uapi_rm() {
fulldomain=$1
txtvalue=$2
_info "Removing TXT record via cPanel UAPI"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _cpanel_uapi_get_root; then
_err "No matching root domain for $fulldomain found"
return 1
fi
if ! _cpanel_uapi_findentry; then
_info "Entry doesn't exist, nothing to delete"
return 0
fi
_debug "Deleting record with line_index=$_line_index"
if ! _cpanel_uapi_get_serial "$_domain"; then
_err "Failed to get zone serial for $_domain"
return 1
fi
if ! _cpanel_uapi_request "execute/DNS/mass_edit_zone?zone=${_domain}&serial=${_serial}&remove=${_line_index}"; then
_err "Request to remove TXT record failed for zone $_domain"
return 1
fi
_debug "_result: $_result"
if _contains "$_result" '"status":1'; then
_info "TXT record removed successfully"
return 0
fi
_err "Failed to remove TXT record."
_err "Response: $_result"
return 1
}
#################### Private functions below ##################################
_cpanel_uapi_checkcredentials() {
cPanel_Username="${cPanel_Username:-$(_readaccountconf_mutable cPanel_Username)}"
cPanel_Apitoken="${cPanel_Apitoken:-$(_readaccountconf_mutable cPanel_Apitoken)}"
cPanel_Hostname="${cPanel_Hostname:-$(_readaccountconf_mutable cPanel_Hostname)}"
if [ -z "$cPanel_Username" ] || [ -z "$cPanel_Apitoken" ] || [ -z "$cPanel_Hostname" ]; then
cPanel_Username=""
cPanel_Apitoken=""
cPanel_Hostname=""
_err "You haven't specified cPanel_Username, cPanel_Apitoken, and cPanel_Hostname."
return 1
fi
# Remove trailing slash from hostname if present
cPanel_Hostname=$(echo "$cPanel_Hostname" | sed 's|/$||')
_saveaccountconf_mutable cPanel_Username "$cPanel_Username"
_saveaccountconf_mutable cPanel_Apitoken "$cPanel_Apitoken"
_saveaccountconf_mutable cPanel_Hostname "$cPanel_Hostname"
if [ -n "$cPanel_TTL" ]; then
case "$cPanel_TTL" in
*[!0-9]*)
_info "Ignoring invalid cPanel_TTL: $cPanel_TTL"
cPanel_TTL=""
;;
*)
_saveaccountconf_mutable cPanel_TTL "$cPanel_TTL"
;;
esac
fi
return 0
}
_cpanel_uapi_request() {
export _H1="Authorization: cpanel $cPanel_Username:$cPanel_Apitoken"
_result=$(_get "$cPanel_Hostname/$1")
return $?
}
_cpanel_uapi_get_root() {
if ! _cpanel_uapi_checkcredentials; then return 1; fi
if ! _cpanel_uapi_request "execute/DomainInfo/list_domains"; then
_err "Request to cPanel API failed while listing domains"
return 1
fi
_debug "DomainInfo response length: ${#_result}"
if ! _contains "$_result" '"status":1'; then
_err "cPanel UAPI request failed. Is the API token correct?"
_debug "Response: $_result"
return 1
fi
# Extract main_domain
_main_domain=$(echo "$_result" | _egrep_o '"main_domain":"[^"]*"' | _head_n 1 | sed 's/.*"main_domain":"//;s/"//')
_debug "main_domain: $_main_domain"
# Extract addon_domains (array of strings)
_addon_domains=$(echo "$_result" | _egrep_o '"addon_domains":\[[^]]*\]' | sed 's/.*"addon_domains":\[//;s/\]$//' | _egrep_o '"[a-zA-Z0-9._-]+"' | sed 's/"//g')
_debug "addon_domains: $_addon_domains"
# Build list of all domains to check
_all_domains="$_main_domain $_addon_domains"
_debug "All domains: $_all_domains"
# Find the matching root domain (prefer longest match)
_best_match=""
_best_len=0
for _check_domain in $_all_domains; do
if [ -z "$_check_domain" ]; then continue; fi
if _endswith "$fulldomain" "$_check_domain"; then
_len=${#_check_domain}
if [ "$_len" -gt "$_best_len" ]; then
_best_match="$_check_domain"
_best_len="$_len"
fi
fi
done
if [ -n "$_best_match" ]; then
_domain="$_best_match"
_debug "Root domain: $_domain"
return 0
fi
return 1
}
_cpanel_uapi_get_serial() {
_zone="$1"
if ! _cpanel_uapi_request "execute/DNS/parse_zone?zone=${_zone}"; then
_err "Request to parse zone failed for $_zone"
return 1
fi
# Split JSON records onto separate lines using a POSIX-portable sed literal newline
# (\\n in sed replacement is a GNU/BusyBox extension; a backslash-newline works everywhere)
_soa_line=$(echo "$_result" | sed 's/},{/},\
{/g' | grep '"record_type":"SOA"' | _head_n 1)
_debug "SOA line: $_soa_line"
if [ -z "$_soa_line" ]; then
_err "SOA record not found for zone $_zone"
_debug "parse_zone response: $_result"
return 1
fi
# Extract the third element from data_b64 array (serial is index 2, 0-based)
# data_b64 format: ["ns","admin","SERIAL","refresh","retry","expire","minimum"]
_serial_b64=$(echo "$_soa_line" | _egrep_o '"data_b64":\[[^]]*\]' | sed 's/"data_b64":\[//;s/\]//' | sed 's/"//g' | cut -d',' -f3)
_debug "serial_b64: $_serial_b64"
if [ -z "$_serial_b64" ]; then
_err "Could not extract serial from SOA record"
return 1
fi
_serial=$(printf '%s' "$_serial_b64" | _dbase64)
_debug "Decoded serial: $_serial"
if [ -z "$_serial" ]; then
_err "Failed to decode serial"
return 1
fi
return 0
}
_cpanel_uapi_findentry() {
_debug "Finding TXT entry for $fulldomain with value $txtvalue"
if ! _cpanel_uapi_request "execute/DNS/parse_zone?zone=${_domain}"; then
_err "Request to parse zone failed for $_domain"
return 1
fi
_debug "parse_zone result length: ${#_result}"
# Base64-encode the txtvalue to match against data_b64 in the response
_b64_txtvalue=$(printf '%s' "$txtvalue" | _base64)
_debug "b64_txtvalue: $_b64_txtvalue"
# Split records onto separate lines, find matching TXT record by base64 value
_line_index=$(echo "$_result" | sed 's/},{/},\
{/g' | grep '"record_type":"TXT"' | grep -F "$_b64_txtvalue" | _egrep_o '"line_index":[0-9]+' | _head_n 1 | cut -d: -f2)
_debug "line_index: $_line_index"
if [ -n "$_line_index" ]; then
_debug "Entry found with line_index=$_line_index"
return 0
fi
return 1
}

181
dnsapi/dns_creoline.sh Normal file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_creoline_info='creoline
Site: https://www.creoline.com/de
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_creoline
Help: https://help.creoline.com
Options:
creolineApiToken
creolineApiSecret
Issues: github.com/acmesh-official/acme.sh/issues/7103
'
creolineApi="https://api.creoline.com/v1"
######## Public functions #####################
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPB8"
dns_creoline_add() {
fulldomain=$1
txtvalue=$2
creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}"
creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}"
if [ -z "$creolineApiToken" ] || [ -z "$creolineApiSecret" ]; then
_err "Error required creoline API Token or creoline API Secret not specified."
_err "Please set it with the Command 'export creolineApiToken=<YourToken>' and 'export creolineApiSecret=<YourSecret>'."
return 1
else
_saveaccountconf_mutable creolineApiToken "$creolineApiToken"
_saveaccountconf_mutable creolineApiSecret "$creolineApiSecret"
fi
_debug "Detecting the root dns zone."
if ! _get_root "$fulldomain"; then
_err "Error on detecting the root dns zone."
return 1
fi
_info "Adding record"
if _creoline_rest POST "dns/zone/$_domain/record" "{\"type\":\"TXT\",\"host\":\"$_sub_domain\",\"record\":\"$txtvalue\",\"ttl\":\"60\"}"; then
if _contains "$response" "$txtvalue"; then
_info "Added, OK"
return 0
else
_err "Add txt record error."
return 1
fi
fi
_err "Add txt record error."
return 1
}
#fulldomain txtvalue
dns_creoline_rm() {
fulldomain=$1
txtvalue=$2
creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}"
creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}"
_debug "Detecting the root dns zone."
if ! _get_root "$fulldomain"; then
_err "Error on detecting the root dns zone."
return 1
fi
_info "Getting earlier created txt record."
if ! _creoline_rest GET "dns/zone/$_domain/record/type/TXT/record/$txtvalue"; then
if _contains "$response" "errors" || _contains "$response" "message"; then
_err "Error on getting earlier created txt record."
return 1
fi
_err "Error on getting earlier created txt record."
return 1
fi
record_id=$(echo "$response" | _egrep_o "\"id\"[ ]*:[ ]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
_debug "record_id" "$record_id"
if [ -z "$record_id" ]; then
_err "Error on deleting earlier created txt record. No record id found in response."
return 1
fi
_info "Deleting earlier created txt record."
if ! _creoline_rest DELETE "dns/zone/$_domain/record/$record_id"; then
if _contains "$response" "errors" || _contains "$response" "message"; then
_err "Error on deleting earlier created txt record."
return 1
fi
_err "Error on deleting earlier created txt record."
return 1
fi
_info "Deleted, OK"
return 0
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
_get_root() {
domain=$1
if ! _creoline_rest GET "dns/zone/root/$domain"; then
return 1
fi
_sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
_debug _sub_domain "$_sub_domain"
_domain=$(echo "$response" | _egrep_o "\"domain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
_debug _domain "$_domain"
if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then
return 1
fi
}
_creoline_rest() {
method=$1
uri="$2"
data="$3"
timestamp=$(_time)
canonical_request="${timestamp}.${creolineApi}/${uri}"
signature_hash=$(printf "%s" "$canonical_request" | _hmac sha256 "$(printf "%s" "$creolineApiSecret" | _hex_dump | tr -d " ")" hex)
_debug method "$method"
_debug uri "$uri"
_debug data "$data"
_debug2 timestamp "$timestamp"
_debug2 canonical_request "$canonical_request"
_debug2 signature_hash "$signature_hash"
token_trimmed=$(echo "$creolineApiToken" | tr -d '"')
hmac_trimmed=$(echo "$signature_hash" | tr -d '"')
export _H1="Content-Type: application/json"
if [ "$token_trimmed" ]; then
export _H2="X-Api-Token: $token_trimmed"
fi
if [ "$hmac_trimmed" ]; then
export _H3="X-Creoline-Api-Signature: $hmac_trimmed"
fi
if [ "$timestamp" ]; then
export _H4="X-Creoline-Api-Timestamp: $timestamp"
fi
if [ "$method" != "GET" ]; then
response="$(_post "$data" "$creolineApi/$uri" "" "$method")"
else
response="$(_get "$creolineApi/$uri")"
fi
if [ "$?" != "0" ]; then
_err "error $uri"
return 1
fi
_debug response "$response"
if _contains "$response" "errors"; then
error=$(echo "$response" | _egrep_o "\"errors\":[[]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | tr -d "[")
_err "Error: $error"
_err "URI:$uri"
return 1
elif _contains "$response" "message"; then
message=$(echo "$response" | _egrep_o "\"message\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \")
_err "Error: $message"
_err "URI:$uri"
return 1
fi
return 0
}

View file

@ -101,6 +101,8 @@ _cyon_load_parameters() {
# This header is required for curl calls.
_H1="X-Requested-With: XMLHttpRequest"
export _H1
_H3="User-Agent: cyon-dns-acmesh/1.0"
export _H3
}
_cyon_print_header() {
@ -125,7 +127,11 @@ _cyon_print_header() {
}
_cyon_get_cookie_header() {
printf "Cookie: %s" "$(grep "cyon=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'cyon=[^;]*;' | tr -d ';')"
# Extract all cookies from the response headers (case-insensitive)
_cookies="$(grep -i "^set-cookie:" "$HTTP_HEADER" | sed 's/^[Ss]et-[Cc]ookie: //' | sed 's/;.*//' | tr '\n' '; ' | sed 's/; $//')"
if [ -n "$_cookies" ]; then
printf "Cookie: %s" "$_cookies"
fi
}
_cyon_login() {
@ -155,7 +161,12 @@ _cyon_login() {
_get "https://my.cyon.ch/" >/dev/null
# todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request.
# Update cookie after loading main page (only if new cookies are set)
_new_cookies="$(_cyon_get_cookie_header)"
if [ -n "$_new_cookies" ]; then
_H2="$_new_cookies"
export _H2
fi
# 2FA authentication with OTP?
if [ -n "${CY_OTP_Secret}" ]; then
@ -184,6 +195,13 @@ _cyon_login() {
fi
_info " success"
# Update cookie after 2FA (only if new cookies are set)
_new_cookies="$(_cyon_get_cookie_header)"
if [ -n "$_new_cookies" ]; then
_H2="$_new_cookies"
export _H2
fi
fi
_info ""
@ -205,7 +223,17 @@ _cyon_change_domain_env() {
domain_env="$(printf "%s" "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')"
_debug "Changing domain environment to ${domain_env}"
gloo_item_key="$(_get "https://my.cyon.ch/domain/" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")"
domain_page_response="$(_get "https://my.cyon.ch/domain/")"
_debug domain_page_response "${domain_page_response}"
# Check if we got an error response (JSON) instead of HTML
if printf "%s" "${domain_page_response}" | grep -q '"iserror":true'; then
_err " $(printf "%s" "${domain_page_response}" | _cyon_get_response_message)"
_err ""
return 1
fi
gloo_item_key="$(printf "%s" "${domain_page_response}" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")"
_debug gloo_item_key "${gloo_item_key}"
domain_env_url="https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/${gloo_item_key}"
@ -257,15 +285,15 @@ _cyon_delete_txt() {
list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async"
list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')"
list_txt_response="$(_get "${list_txt_url}")"
_debug list_txt_response "${list_txt_response}"
if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi
# Find and delete all acme challenge entries for the $fulldomain.
_dns_entries="$(printf "%b\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')"
_dns_entries="$(printf "%s\n" "${list_txt_response}" | _egrep_o 'data-hash=\\"[^"]*\\" data-identifier=\\"[^"]*\\"' | sed 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\"/\1 \2/')"
printf "%s" "${_dns_entries}" | while read -r _hash _identifier; do
printf "%s\n" "${_dns_entries}" | while read -r _hash _identifier; do
dns_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)"
dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)"
@ -304,11 +332,11 @@ _cyon_get_response_message() {
}
_cyon_get_response_status() {
_egrep_o '"status":[a-zA-z0-9]*' | cut -d : -f 2
_egrep_o '"status":[a-zA-Z0-9]*' | cut -d : -f 2
}
_cyon_get_validation_status() {
_egrep_o '"valid":[a-zA-z0-9]*' | cut -d : -f 2
_egrep_o '"valid":[a-zA-Z0-9]*' | cut -d : -f 2
}
_cyon_get_response_success() {
@ -316,7 +344,7 @@ _cyon_get_response_success() {
}
_cyon_get_environment_change_status() {
_egrep_o '"authenticated":[a-zA-z0-9]*' | cut -d : -f 2
_egrep_o '"authenticated":[a-zA-Z0-9]*' | cut -d : -f 2
}
_cyon_check_if_2fa_missed() {

204
dnsapi/dns_czechia.sh Normal file
View file

@ -0,0 +1,204 @@
#!/usr/bin/env sh
# dns_czechia.sh - CZECHIA.COM/ZONER DNS API for acme.sh (DNS-01)
#
# Documentation: https://api.czechia.com/swagger/index.html
#shellcheck disable=SC2034
dns_czechia_info='[
{"name":"CZ_AuthorizationToken","usage":"Your API token from CZECHIA.COM/Zoner administration.","required":"1"},
{"name":"CZ_Zones","usage":"Managed zones separated by comma or space (e.g. \"example.com\").","required":"1"},
{"name":"CZ_API_BASE","usage":"Defaults to https://api.czechia.com","required":"0"}
]'
dns_czechia_add() {
fulldomain="$1"
txtvalue="$2"
_debug "dns_czechia_add fulldomain='$fulldomain'"
if [ -z "$fulldomain" ] || [ -z "$txtvalue" ]; then
_err "dns_czechia_add: missing fulldomain or txtvalue"
return 1
fi
_czechia_load_conf || return 1
_current_zone=$(_czechia_pick_zone "$fulldomain")
if [ -z "$_current_zone" ]; then
_err "No matching zone found for $fulldomain. Please check CZ_Zones."
return 1
fi
_czechia_tab="$(printf '\t')"
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//")
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
_err "Missing zone or CZ_AuthorizationToken."
return 1
fi
_url="$CZ_API_BASE/api/DNS/$_cz/TXT"
_fd=$(printf "%s" "$fulldomain" | _lower_case | sed 's/\.$//')
if [ "$_fd" = "$_cz" ]; then
_h="@"
else
# Remove the literal ".<zone>" suffix from _fd, if present
_h=${_fd%."$_cz"}
[ "$_h" = "$_fd" ] && _h="@"
fi
[ -z "$_h" ] && _h="@"
_info "Adding TXT record for $_h in zone $_cz"
_h_esc=$(printf "%s" "$_h" | sed 's/\\/\\\\/g; s/"/\\"/g')
_txt_esc=$(printf "%s" "$txtvalue" | sed 's/\\/\\\\/g; s/"/\\"/g')
_body="{\"hostName\":\"$_h_esc\",\"text\":\"$_txt_esc\",\"ttl\":300,\"publishZone\":1}"
_debug "URL: $_url"
_debug "Body: $_body"
export _H1="Content-Type: application/json"
export _H2="AuthorizationToken: $_tk"
_res="$(_post "$_body" "$_url" "" "POST")"
_post_exit="$?"
_debug2 "Response: $_res"
if [ "$_post_exit" -ne 0 ]; then
_err "API request failed. exit code $_post_exit"
return 1
fi
if _contains "$_res" "already exists"; then
_info "Record already exists, skipping."
return 0
fi
_nres="$(printf '%s' "$_res" | _normalizeJson)"
if [ "$?" -ne 0 ] || [ -z "$_nres" ]; then
_nres="$_res"
fi
if _contains "$_nres" "\"status\":4" || _contains "$_nres" "\"status\":5" || _contains "$_nres" "\"errors\""; then
_err "API error: $_res"
return 1
fi
return 0
}
dns_czechia_rm() {
fulldomain="$1"
txtvalue="$2"
_debug "dns_czechia_rm fulldomain='$fulldomain'"
if [ -z "$fulldomain" ] || [ -z "$txtvalue" ]; then
_err "dns_czechia_rm: missing fulldomain or txtvalue"
return 1
fi
_czechia_load_conf || return 1
_current_zone=$(_czechia_pick_zone "$fulldomain")
if [ -z "$_current_zone" ]; then
_err "No matching zone found for $fulldomain. Please check CZ_Zones configuration."
return 1
fi
_czechia_tab="$(printf '\t')"
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//")
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
_err "Missing zone or CZ_AuthorizationToken."
return 1
fi
_url="$CZ_API_BASE/api/DNS/$_cz/TXT"
_fd=$(printf "%s" "$fulldomain" | _lower_case | sed 's/\.$//')
if [ "$_fd" = "$_cz" ]; then
_h="@"
else
_h=$(printf "%s" "$_fd" | sed "s/\.$_cz$//")
[ "$_h" = "$_fd" ] && _h="@"
fi
[ -z "$_h" ] && _h="@"
_h_esc=$(printf "%s" "$_h" | sed 's/\\/\\\\/g; s/"/\\"/g')
_txt_esc=$(printf "%s" "$txtvalue" | sed 's/\\/\\\\/g; s/"/\\"/g')
_body="{\"hostName\":\"$_h_esc\",\"text\":\"$_txt_esc\",\"ttl\":300,\"publishZone\":1}"
_debug "URL: $_url"
_debug "Body: $_body"
export _H1="Content-Type: application/json"
export _H2="AuthorizationToken: $_tk"
_res="$(_post "$_body" "$_url" "" "DELETE")"
_post_exit="$?"
_debug2 "Response: $_res"
if [ "$_post_exit" -ne 0 ]; then
_err "CZECHIA DNS API DELETE request failed for $_fd: exit code $_post_exit, response: $_res"
return 1
fi
_res_normalized=$(printf '%s' "$_res" | _normalizeJson)
if _contains "$_res_normalized" '"isError":true'; then
_err "CZECHIA DNS API reported an error while deleting TXT for $_fd: $_res"
return 1
fi
return 0
}
_czechia_load_conf() {
CZ_AuthorizationToken="${CZ_AuthorizationToken:-$(_readaccountconf_mutable CZ_AuthorizationToken)}"
if [ -z "$CZ_AuthorizationToken" ]; then
_err "Missing CZ_AuthorizationToken"
return 1
fi
CZ_Zones="${CZ_Zones:-$(_readaccountconf_mutable CZ_Zones)}"
if [ -z "$CZ_Zones" ]; then
_err "Missing CZ_Zones"
return 1
fi
CZ_API_BASE="${CZ_API_BASE:-$(_readaccountconf_mutable CZ_API_BASE)}"
[ -z "$CZ_API_BASE" ] && CZ_API_BASE="https://api.czechia.com"
_saveaccountconf_mutable CZ_AuthorizationToken "$CZ_AuthorizationToken"
_saveaccountconf_mutable CZ_Zones "$CZ_Zones"
_saveaccountconf_mutable CZ_API_BASE "$CZ_API_BASE"
return 0
}
_czechia_pick_zone() {
_czechia_pz_tab="$(printf '\t')"
_fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//')
_best_zone=""
_zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g')
for _z in $_zones_space; do
_clean_z=$(printf "%s" "$_z" | _lower_case | sed "s/[ $_czechia_pz_tab]//g; s/\.\$//")
[ -z "$_clean_z" ] && continue
case "$_fd" in
"$_clean_z" | *."$_clean_z")
if [ ${#_clean_z} -gt ${#_best_zone} ]; then
_best_zone="$_clean_z"
fi
;;
esac
done
printf "%s" "$_best_zone"
}

View file

@ -4,7 +4,7 @@ dns_da_info='DirectAdmin Server API
Site: DirectAdmin.com/api.php
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_da
Options:
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443"
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443". Special characters in the user/password must be percent-encoded, e.g. "@" -> "%40".
DA_Api_Insecure Insecure TLS. 0: check for cert validity, 1: always accept
Issues: github.com/TigerP/acme.sh/issues
'

View file

@ -4,7 +4,7 @@ dns_desec_info='deSEC.io
Site: desec.readthedocs.io/en/latest/
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_desec
Options:
DDNSS_Token API Token
DEDYN_TOKEN API Token
Issues: github.com/acmesh-official/acme.sh/issues/2180
Author: Zheng Qian
'
@ -39,6 +39,7 @@ dns_desec_add() {
_err "invalid domain"
return 1
fi
_sub_domain=$(echo "$_sub_domain" | _lower_case)
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
@ -48,7 +49,7 @@ dns_desec_add() {
_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")"
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
_debug "existing TXT found"
_debug oldtxtvalues "$oldtxtvalues"
if [ -n "$oldtxtvalues" ]; then
@ -100,7 +101,7 @@ dns_desec_rm() {
_err "invalid domain"
return 1
fi
_sub_domain=$(echo "$_sub_domain" | _lower_case)
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
@ -110,7 +111,7 @@ dns_desec_rm() {
_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")"
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
_debug "existing TXT found"
_debug oldtxtvalues "$oldtxtvalues"
if [ -n "$oldtxtvalues" ]; then
@ -150,6 +151,8 @@ _desec_rest() {
if [ "$m" != "GET" ]; then
_secure_debug2 data "$data"
response="$(_post "$data" "$ep" "" "$m")"
_info "Sleeping 1s to respect deSEC write rate limit"
_sleep 1
else
response="$(_get "$ep")"
fi

View file

@ -5,14 +5,11 @@ Site: DNSExit.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsexit
Options:
DNSEXIT_API_KEY API Key
DNSEXIT_AUTH_USER Username
DNSEXIT_AUTH_PASS Password
Issues: github.com/acmesh-official/acme.sh/issues/4719
Author: Samuel Jimenez
'
DNSEXIT_API_URL="https://api.dnsexit.com/dns/"
DNSEXIT_HOSTS_URL="https://update.dnsexit.com/ipupdate/hosts.jsp"
######## Public functions #####################
#Usage: dns_dnsexit_add _acme-challenge.*.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
@ -28,20 +25,7 @@ dns_dnsexit_add() {
return 1
fi
_debug 'First detect the root zone'
if ! _get_root "$fulldomain"; then
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"add\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":0,\"overwrite\":false}}"; then
_err "$response"
return 1
fi
_debug2 _response "$response"
return 0
_dnsexit_zone_op add ',"ttl":1,"overwrite":false'
}
#Usage: fulldomain txtvalue
@ -58,54 +42,43 @@ dns_dnsexit_rm() {
return 1
fi
_debug 'First detect the root zone'
if ! _get_root "$fulldomain"; then
_err "$response"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"delete\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"}}"; then
_err "$response"
return 1
fi
_debug2 _response "$response"
return 0
_dnsexit_zone_op delete ''
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
_get_root() {
domain=$1
# The legacy zone-detection endpoint (update.dnsexit.com/ipupdate/hosts.jsp)
# was shut down by DNSExit and now returns 503, and the JSON API offers no
# zone-list call. So find the root zone by attempting the actual operation at
# each domain level: the API answers "code":0 only when the domain matches a
# zone of the account. https://github.com/acmesh-official/acme.sh/issues/6914
#Usage: _dnsexit_zone_op <add|delete> <extra-json-fields>
_dnsexit_zone_op() {
_op="$1"
_extra="$2"
i=1
while true; do
_domain=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$_domain"
_domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
_debug _domain "$_domain"
if [ -z "$_domain" ]; then
_err "Could not find the root zone of $fulldomain in your DNSExit account"
return 1
fi
_debug login "$DNSEXIT_AUTH_USER"
_debug password "$DNSEXIT_AUTH_PASS"
_debug domain "$_domain"
_sub_domain="$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//")"
if [ "$_sub_domain" = "$fulldomain" ]; then
_sub_domain=""
fi
_debug _sub_domain "$_sub_domain"
_dnsexit_http "login=$DNSEXIT_AUTH_USER&password=$DNSEXIT_AUTH_PASS&domain=$_domain"
if _contains "$response" "0=$_domain"; then
_sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")"
return 0
else
_debug "Go to next level of $_domain"
if _dnsexit_rest "{\"domain\":\"$_domain\",\"$_op\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"$_extra}}"; then
if _contains "$response" "\"code\":0" || _contains "$response" "\"code\": 0"; then
_debug2 _response "$response"
return 0
fi
_debug "Zone $_domain was not accepted, trying the next level" "$response"
fi
i=$(_math "$i" + 1)
done
return 1
}
_dnsexit_rest() {
@ -136,27 +109,7 @@ _dnsexit_rest() {
return 0
}
_dnsexit_http() {
m=GET
param="$1"
_debug param "$param"
_debug get "$DNSEXIT_HOSTS_URL?$param"
response="$(_get "$DNSEXIT_HOSTS_URL?$param")"
_debug response "$response"
if [ "$?" != "0" ]; then
_err "Error $param"
return 1
fi
_debug2 response "$response"
return 0
}
get_account_info() {
DNSEXIT_API_KEY="${DNSEXIT_API_KEY:-$(_readaccountconf_mutable DNSEXIT_API_KEY)}"
if test -z "$DNSEXIT_API_KEY"; then
DNSEXIT_API_KEY=''
@ -166,23 +119,5 @@ get_account_info() {
_saveaccountconf_mutable DNSEXIT_API_KEY "$DNSEXIT_API_KEY"
DNSEXIT_AUTH_USER="${DNSEXIT_AUTH_USER:-$(_readaccountconf_mutable DNSEXIT_AUTH_USER)}"
if test -z "$DNSEXIT_AUTH_USER"; then
DNSEXIT_AUTH_USER=""
_err 'DNSEXIT_AUTH_USER was not exported'
return 1
fi
_saveaccountconf_mutable DNSEXIT_AUTH_USER "$DNSEXIT_AUTH_USER"
DNSEXIT_AUTH_PASS="${DNSEXIT_AUTH_PASS:-$(_readaccountconf_mutable DNSEXIT_AUTH_PASS)}"
if test -z "$DNSEXIT_AUTH_PASS"; then
DNSEXIT_AUTH_PASS=""
_err 'DNSEXIT_AUTH_PASS was not exported'
return 1
fi
_saveaccountconf_mutable DNSEXIT_AUTH_PASS "$DNSEXIT_AUTH_PASS"
return 0
}

View file

@ -5,6 +5,7 @@ Site: DNSimple.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple
Options:
DNSimple_OAUTH_TOKEN OAuth Token
DNSimple_ACCOUNT_ID Account ID. Optional, only needed when the token can access multiple accounts.
Issues: github.com/pho3nixf1re/acme.sh/issues
'
@ -17,6 +18,7 @@ dns_dnsimple_add() {
fulldomain=$1
txtvalue=$2
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
DNSimple_OAUTH_TOKEN=""
_err "You have not set the dnsimple oauth token yet."
@ -25,10 +27,10 @@ dns_dnsimple_add() {
fi
# save the oauth token for later
_saveaccountconf DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
_saveaccountconf_mutable DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
if ! _get_account_id; then
_err "failed to retrive account id"
_err "failed to retrieve account id"
return 1
fi
@ -56,8 +58,14 @@ dns_dnsimple_add() {
dns_dnsimple_rm() {
fulldomain=$1
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
_err "You have not set the dnsimple oauth token yet."
return 1
fi
if ! _get_account_id; then
_err "failed to retrive account id"
_err "failed to retrieve account id"
return 1
fi
@ -122,13 +130,16 @@ _get_root() {
# returns _account_id
_get_account_id() {
_debug "retrive account id"
if ! _dnsimple_rest GET "whoami"; then
return 1
DNSimple_ACCOUNT_ID="${DNSimple_ACCOUNT_ID:-$(_readaccountconf_mutable DNSimple_ACCOUNT_ID)}"
if [ "$DNSimple_ACCOUNT_ID" ]; then
_saveaccountconf_mutable DNSimple_ACCOUNT_ID "$DNSimple_ACCOUNT_ID"
_account_id="$DNSimple_ACCOUNT_ID"
_debug _account_id "$_account_id"
return 0
fi
if _contains "$response" "\"account\":null"; then
_err "no account associated with this token"
_debug "retrieve account id"
if ! _dnsimple_rest GET "whoami"; then
return 1
fi
@ -137,7 +148,25 @@ _get_account_id() {
return 1
fi
if _contains "$response" "\"account\":null"; then
# the whoami of a user token (dnsimple_u_*) carries no account,
# so list the accounts the token can access instead
# https://github.com/acmesh-official/acme.sh/issues/6491
if ! _dnsimple_rest GET "accounts"; then
return 1
fi
fi
_account_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"email\":" | cut -d: -f2 | cut -d, -f1)
if [ -z "$_account_id" ]; then
_err "no account associated with this token"
return 1
fi
if [ "$(echo "$_account_id" | wc -l)" -gt 1 ]; then
_err "The token has access to multiple accounts, please pick one and set it explicitly:"
_err "export DNSimple_ACCOUNT_ID=<one of: $(echo "$_account_id" | tr '\n' ' ')>"
return 1
fi
_debug _account_id "$_account_id"
return 0

View file

@ -23,6 +23,8 @@ dns_dynu_add() {
fulldomain=$1
txtvalue=$2
Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}"
Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}"
if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then
Dynu_ClientId=""
Dynu_Secret=""
@ -32,8 +34,8 @@ dns_dynu_add() {
fi
#save the client id and secret to the account conf file.
_saveaccountconf Dynu_ClientId "$Dynu_ClientId"
_saveaccountconf Dynu_Secret "$Dynu_Secret"
_saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId"
_saveaccountconf_mutable Dynu_Secret "$Dynu_Secret"
if [ -z "$Dynu_Token" ]; then
_info "Getting Dynu token."
@ -69,6 +71,8 @@ dns_dynu_rm() {
fulldomain=$1
txtvalue=$2
Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}"
Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}"
if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then
Dynu_ClientId=""
Dynu_Secret=""
@ -78,8 +82,8 @@ dns_dynu_rm() {
fi
#save the client id and secret to the account conf file.
_saveaccountconf Dynu_ClientId "$Dynu_ClientId"
_saveaccountconf Dynu_Secret "$Dynu_Secret"
_saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId"
_saveaccountconf_mutable Dynu_Secret "$Dynu_Secret"
if [ -z "$Dynu_Token" ]; then
_info "Getting Dynu token."
@ -214,11 +218,11 @@ _dynu_authentication() {
response="$(_get "$Dynu_EndPoint/oauth2/token")"
if [ "$?" != "0" ]; then
_err "Authentication failed."
_err "Authentication failed: no response from $Dynu_EndPoint/oauth2/token"
return 1
fi
if _contains "$response" "Authentication Exception"; then
_err "Authentication failed."
_err "Authentication failed. Server response: $response"
return 1
fi
if _contains "$response" "access_token"; then

View file

@ -107,7 +107,7 @@ _get_domain() {
return 0
fi
done
_err "Either their is no such host on your dnyv6 account or it cannot be accessed with this key"
_err "Either there is no such host on your dynv6 account, or it cannot be accessed with this key"
return 1
}
@ -179,8 +179,8 @@ _dns_dynv6_rm_http() {
fi
}
#Usage: _get_zone_id $record
#get the zoneid for a specifc record or zone
#usage: _get_zone_id §record
#where $record is the record to get the id for
#returns _zone_id the id of the zone
_get_zone_id() {
@ -189,7 +189,6 @@ _get_zone_id() {
_dynv6_rest GET zones
zones="$(echo "$response" | tr '}' '\n' | tr ',' '\n' | grep name | sed 's/\[//g' | tr -d '{' | tr -d '"')"
#echo $zones
selected=""
for z in $zones; do
@ -217,9 +216,9 @@ _get_zone_name() {
_zone_name="${_zone_name#name:}"
}
#usaage _get_record_id $zone_id $record
# where zone_id is thevalue returned by _get_zone_id
# and record ist in the form _acme.www for an fqdn of _acme.www.example.com
#usage _get_record_id $zone_id $record
# where zone_id is the value returned by _get_zone_id
# and record is in the form _acme.www for an fqdn of _acme.www.example.com
# returns _record_id
_get_record_id() {
_zone_id="$1"
@ -234,8 +233,7 @@ _get_record_id() {
_get_record_id_from_response() {
response="$1"
_record_id="$(echo "$response" | tr '}' '\n' | grep "\"name\":\"$record\"" | grep "\"data\":\"$value\"" | tr ',' '\n' | grep id | tr -d '"' | tr -d 'id:')"
#_record_id="${_record_id#id:}"
_record_id="$(echo "$response" | tr '}' '\n' | grep "\"name\":\"$record\"" | grep "\"data\":\"$value\"" | tr ',' '\n' | grep '"id":' | tr -d '"' | tr -d 'id:' | tr -d '{')"
if [ -z "$_record_id" ]; then
_err "no such record: $record found in zone $_zone_id"
return 1

View file

@ -363,17 +363,12 @@ _edgedns_rest() {
_edgedns_eg_timestamp() {
_debug "Generating signature Timestamp"
_debug3 "Retriving ntp time"
_timeheaders="$(_get "https://www.ntp.org" "onlyheader")"
_debug3 "_timeheaders" "$_timeheaders"
_ntpdate="$(echo "$_timeheaders" | grep -i "Date:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n")"
_debug3 "_ntpdate" "$_ntpdate"
_ntpdate="$(echo "${_ntpdate}" | sed -e 's/^[[:space:]]*//')"
_debug3 "_NTPDATE" "$_ntpdate"
_ntptime="$(echo "${_ntpdate}" | _head_n 1 | cut -d " " -f 5 | tr -d "\r\n")"
_debug3 "_ntptime" "$_ntptime"
_eg_timestamp=$(date -u "+%Y%m%dT")
_eg_timestamp="$(printf "%s%s+0000" "$_eg_timestamp" "$_ntptime")"
#Akamai accepts a clock skew of +/-30s, so use the system clock directly.
#The previous code fetched the Date header from www.ntp.org, which is not
#a reliable time source (it served a wrong time for hours, issue 3973),
#cost an extra https round-trip for every API request, and combined the
#remote time of day with the LOCAL date, breaking around UTC midnight.
_eg_timestamp="$(date -u "+%Y%m%dT%H:%M:%S+0000")"
_debug "_eg_timestamp" "$_eg_timestamp"
}

139
dnsapi/dns_efficientip.sh Executable file
View file

@ -0,0 +1,139 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_efficientip_info='efficientip.com
Site: https://efficientip.com/
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_efficientip
Options:
EfficientIP_Creds HTTP Basic Authentication credentials. E.g. "username:password"
EfficientIP_Server EfficientIP SOLIDserver Management IP address or FQDN.
EfficientIP_DNS_Name Name of the DNS smart or server hosting the zone. Optional.
EfficientIP_View Name of the DNS view hosting the zone. Optional.
OptionsAlt:
EfficientIP_Token_Key Alternative API token key, prefered over basic authentication.
EfficientIP_Token_Secret Alternative API token secret, required when using a token key.
EfficientIP_Server EfficientIP SOLIDserver Management IP address or FQDN.
EfficientIP_DNS_Name Name of the DNS smart or server hosting the zone. Optional.
EfficientIP_View Name of the DNS view hosting the zone. Optional.
Issues: github.com/acmesh-official/acme.sh/issues/6325
Author: EfficientIP-Labs <contact@efficientip.com>
'
dns_efficientip_add() {
fulldomain=$1
txtvalue=$2
_info "Using EfficientIP API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if { [ -z "${EfficientIP_Creds}" ] && { [ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ]; }; } || [ -z "${EfficientIP_Server}" ]; then
EfficientIP_Creds=""
EfficientIP_Token_Key=""
EfficientIP_Token_Secret=""
EfficientIP_Server=""
_err "You didn't specify any EfficientIP credentials or token or server (EfficientIP_Creds; EfficientIP_Token_Key; EfficientIP_Token_Secret; EfficientIP_Server)."
_err "Please set them via EXPORT EfficientIP_Creds=username:password or EXPORT EfficientIP_server=ip/hostname"
_err "or if you want to use Token instead EXPORT EfficientIP_Token_Key=yourkey"
_err "and EXPORT EfficientIP_Token_Secret=yoursecret"
_err "then try again."
return 1
fi
if [ -z "${EfficientIP_DNS_Name}" ]; then
EfficientIP_DNS_Name=""
fi
EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode)
if [ -z "${EfficientIP_View}" ]; then
EfficientIP_View=""
fi
EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode)
_saveaccountconf EfficientIP_Creds "${EfficientIP_Creds}"
_saveaccountconf EfficientIP_Token_Key "${EfficientIP_Token_Key}"
_saveaccountconf EfficientIP_Token_Secret "${EfficientIP_Token_Secret}"
_saveaccountconf EfficientIP_Server "${EfficientIP_Server}"
_saveaccountconf EfficientIP_DNS_Name "${EfficientIP_DNS_Name}"
_saveaccountconf EfficientIP_View "${EfficientIP_View}"
export _H1="Accept-Language:en-US"
baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?rr_type=TXT&rr_ttl=300&rr_name=${fulldomain}&rr_value1=${txtvalue}"
if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then
baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}"
fi
if [ "${EfficientIP_ViewEncoded}" != "" ]; then
baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}"
fi
if [ -z "${EfficientIP_Token_Secret}" ] || [ -z "${EfficientIP_Token_Key}" ]; then
EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64)
export _H2="Authorization: Basic ${EfficientIP_CredsEncoded}"
else
TS=$(date +%s)
Sig=$(printf "%b\n$TS\nPOST\n$baseurlnObject" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex)
EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig")
export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}"
export _H3="X-SDS-TS: ${TS}"
fi
result="$(_post "" "${baseurlnObject}" "" "POST")"
if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then
_info "DNS record successfully created"
return 0
else
_err "Error creating DNS record"
_err "${result}"
return 1
fi
}
dns_efficientip_rm() {
fulldomain=$1
txtvalue=$2
_info "Using EfficientIP API"
_debug fulldomain "${fulldomain}"
_debug txtvalue "${txtvalue}"
EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode)
EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode)
EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64)
export _H1="Accept-Language:en-US"
baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_delete?rr_type=TXT&rr_name=$fulldomain&rr_value1=$txtvalue"
if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then
baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}"
fi
if [ "${EfficientIP_ViewEncoded}" != "" ]; then
baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}"
fi
if [ -z "$EfficientIP_Token_Secret" ] || [ -z "$EfficientIP_Token_Key" ]; then
EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64)
export _H2="Authorization: Basic $EfficientIP_CredsEncoded"
else
TS=$(date +%s)
Sig=$(printf "%b\n$TS\nDELETE\n${baseurlnObject}" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex)
EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig")
export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}"
export _H3="X-SDS-TS: $TS"
fi
result="$(_post "" "${baseurlnObject}" "" "DELETE")"
if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then
_info "DNS Record successfully deleted"
return 0
else
_err "Error deleting DNS record"
_err "${result}"
return 1
fi
}

267
dnsapi/dns_eurodns.sh Normal file
View file

@ -0,0 +1,267 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_eurodns_info='EuroDNS
Site: eurodns.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_eurodns
Options:
EURODNS_APP_ID Application ID
EURODNS_API_KEY API Key
EURODNS_TTL TTL. Default: "600".
Issues: github.com/acmesh-official/acme.sh/issues
Author: Nicolas Santorelli
'
#
# EuroDNS DNS API
#
# EuroDNS API documentation:
# https://docapi.eurodns.com
#
# Usage:
# export EURODNS_APP_ID="your-app-id"
# export EURODNS_API_KEY="your-api-key"
# acme.sh --issue --dns dns_eurodns -d example.com -d *.example.com
#
# The credentials will be saved in ~/.acme.sh/account.conf
#
# Optional:
# export EURODNS_API_URL="https://rest-api.eurodns.com" # Default API URL
# export EURODNS_TTL=600 # Default TTL (minimum 600 for EuroDNS)
#
EURODNS_API_DEFAULT="https://rest-api.eurodns.com"
EURODNS_TTL_DEFAULT=600
######## Public functions #####################
#Usage: dns_eurodns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_eurodns_add() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue=$2
_info "Using EuroDNS DNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
EURODNS_TTL="${EURODNS_TTL:-$(_readaccountconf_mutable EURODNS_TTL)}"
EURODNS_TTL="${EURODNS_TTL:-$EURODNS_TTL_DEFAULT}"
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
EURODNS_APP_ID=""
EURODNS_API_KEY=""
_err "You didn't specify EuroDNS App ID and API Key."
_err "Please export EURODNS_APP_ID and EURODNS_API_KEY and try again."
return 1
fi
_saveaccountconf_mutable EURODNS_APP_ID "$EURODNS_APP_ID"
_saveaccountconf_mutable EURODNS_API_KEY "$EURODNS_API_KEY"
if [ "$EURODNS_API_URL" != "$EURODNS_API_DEFAULT" ]; then
_saveaccountconf_mutable EURODNS_API_URL "$EURODNS_API_URL"
fi
if [ "$EURODNS_TTL" != "$EURODNS_TTL_DEFAULT" ]; then
_saveaccountconf_mutable EURODNS_TTL "$EURODNS_TTL"
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Invalid domain"
return 1
fi
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
_info "Adding TXT record"
if _eurodns_add_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
_info "Added TXT record successfully."
return 0
else
_err "Failed to add TXT record."
return 1
fi
}
#Usage: fulldomain txtvalue
dns_eurodns_rm() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue=$2
_info "Using EuroDNS DNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
EURODNS_APP_ID=""
EURODNS_API_KEY=""
_err "You didn't specify EuroDNS App ID and API Key."
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Invalid domain"
return 1
fi
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
_info "Removing TXT record"
if _eurodns_rm_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
_info "Removed TXT record successfully."
return 0
else
_err "Failed to remove TXT record."
return 1
fi
}
#################### Private functions below ##################################
# _sub_domain=_acme-challenge.www
# _domain=domain.com
_get_root() {
domain=$1
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
return 1
fi
_eurodns_rest GET "dns-zones/$h"
if [ "$?" != "0" ]; then
if [ "$_code" = "404" ]; then
_debug "Zone $h not found, continuing..."
else
_err "API error looking up zone $h"
return 1
fi
p=$i
i=$(_math "$i" + 1)
continue
fi
if _contains "$response" '"name"'; then
if [ "$i" = "1" ]; then
_sub_domain="@"
else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
fi
_domain=$h
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
_eurodns_add_txt_record() {
domain=$1
subdomain=$2
txtvalue=$3
data='[{"type":"TXT","host":"'"$subdomain"'","rdata":"'"$txtvalue"'","ttl":'"$EURODNS_TTL"'}]'
_debug "Adding TXT record via API"
if _eurodns_rest POST "dns-zones/$domain/dns-records" "$data"; then
if _contains "$response" "$txtvalue"; then
return 0
fi
fi
_err "Failed to add TXT record"
return 1
}
_eurodns_rm_txt_record() {
domain=$1
subdomain=$2
txtvalue=$3
_debug "Getting current zone data for $domain"
if ! _eurodns_rest GET "dns-zones/$domain"; then
_err "Failed to get zone data"
return 1
fi
zone_data=$(echo "$response" | _normalizeJson)
_debug2 zone_data "$zone_data"
# Find the record ID matching our TXT record
record_id=$(echo "$zone_data" | tr '{' '\n' | grep -F '"TXT"' | grep -F "\"$subdomain\"" | grep -F "\"$txtvalue\"" | _egrep_o '"id" *: *[0-9]+' | cut -d : -f 2 | _head_n 1)
_debug record_id "$record_id"
if [ -z "$record_id" ]; then
_info "TXT record not found or already removed"
return 0
fi
_debug "Deleting TXT record $record_id"
if ! _eurodns_rest DELETE "dns-zones/$domain/dns-records/$record_id"; then
_err "Failed to delete TXT record"
return 1
fi
return 0
}
# Usage: _eurodns_rest METHOD ENDPOINT [DATA]
_eurodns_rest() {
method=$1
endpoint=$2
data="$3"
export _H1="X-APP-ID: $EURODNS_APP_ID"
export _H2="X-API-KEY: $EURODNS_API_KEY"
export _H3="Content-Type: application/json"
url="$EURODNS_API_URL/$endpoint"
_debug2 url "$url"
_debug2 method "$method"
_debug2 data "$data"
: >"$HTTP_HEADER"
if [ "$method" = "GET" ]; then
response="$(_get "$url")"
else
response="$(_post "$data" "$url" "" "$method")"
fi
_ret="$?"
unset _H1 _H2 _H3
_debug2 response "$response"
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
_debug2 _code "$_code"
if [ "$_ret" != "0" ]; then
_err "Error calling API: $endpoint"
return 1
fi
if [ "$_code" != "200" ] && [ "$_code" != "201" ] && [ "$_code" != "204" ]; then
if [ "$_code" != "404" ]; then
_err "API error (HTTP $_code): $response"
fi
return 1
fi
return 0
}

226
dnsapi/dns_exoscale.sh Executable file → Normal file
View file

@ -8,9 +8,9 @@ Options:
EXOSCALE_SECRET_KEY API Secret key
'
EXOSCALE_API=https://api.exoscale.com/dns/v1
EXOSCALE_API="https://api-ch-gva-2.exoscale.com/v2"
######## Public functions #####################
######## Public functions ########
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
@ -18,159 +18,197 @@ dns_exoscale_add() {
fulldomain=$1
txtvalue=$2
if ! _checkAuth; then
_debug "Using Exoscale DNS v2 API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _check_auth; then
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
root_domain_id=$(_get_root_domain_id "$fulldomain")
if [ -z "$root_domain_id" ]; then
_err "Unable to determine root domain ID for $fulldomain"
return 1
fi
_debug root_domain_id "$root_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
# Always get the subdomain part first
sub_domain=$(_get_sub_domain "$fulldomain" "$root_domain_id")
_debug sub_domain "$sub_domain"
_info "Adding record"
if _exoscale_rest POST "domains/$_domain_id/records" "{\"record\":{\"name\":\"$_sub_domain\",\"record_type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}}" "$_domain_token"; then
if _contains "$response" "$txtvalue"; then
_info "Added, OK"
return 0
fi
# Build the record name properly
if [ -z "$sub_domain" ]; then
record_name="_acme-challenge"
else
record_name="_acme-challenge.$sub_domain"
fi
_err "Add txt record error."
return 1
payload=$(printf '{"name":"%s","type":"TXT","content":"%s","ttl":120}' "$record_name" "$txtvalue")
_debug payload "$payload"
response=$(_exoscale_rest POST "/dns-domain/${root_domain_id}/record" "$payload")
if _contains "$response" "\"id\""; then
_info "TXT record added successfully."
return 0
else
_err "Error adding TXT record: $response"
return 1
fi
}
# Usage: fulldomain txtvalue
# Used to remove the txt record after validation
dns_exoscale_rm() {
fulldomain=$1
txtvalue=$2
if ! _checkAuth; then
_debug "Using Exoscale DNS v2 API for removal"
_debug fulldomain "$fulldomain"
if ! _check_auth; then
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
root_domain_id=$(_get_root_domain_id "$fulldomain")
if [ -z "$root_domain_id" ]; then
_err "Unable to determine root domain ID for $fulldomain"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug "Getting txt records"
_exoscale_rest GET "domains/${_domain_id}/records?type=TXT&name=$_sub_domain" "" "$_domain_token"
if _contains "$response" "\"name\":\"$_sub_domain\"" >/dev/null; then
_record_id=$(echo "$response" | tr '{' "\n" | grep "\"content\":\"$txtvalue\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \")
record_name="_acme-challenge"
sub_domain=$(_get_sub_domain "$fulldomain" "$root_domain_id")
if [ -n "$sub_domain" ]; then
record_name="_acme-challenge.$sub_domain"
fi
if [ -z "$_record_id" ]; then
_err "Can not get record id to remove."
record_id=$(_find_record_id "$root_domain_id" "$record_name")
if [ -z "$record_id" ]; then
_err "TXT record not found for deletion."
return 1
fi
_debug "Deleting record $_record_id"
if ! _exoscale_rest DELETE "domains/$_domain_id/records/$_record_id" "" "$_domain_token"; then
_err "Delete record error."
response=$(_exoscale_rest DELETE "/dns-domain/$root_domain_id/record/$record_id")
if _contains "$response" "\"state\":\"success\""; then
_info "TXT record deleted successfully."
return 0
else
_err "Error deleting TXT record: $response"
return 1
fi
return 0
}
#################### Private functions below ##################################
######## Private helpers ########
_checkAuth() {
_check_auth() {
EXOSCALE_API_KEY="${EXOSCALE_API_KEY:-$(_readaccountconf_mutable EXOSCALE_API_KEY)}"
EXOSCALE_SECRET_KEY="${EXOSCALE_SECRET_KEY:-$(_readaccountconf_mutable EXOSCALE_SECRET_KEY)}"
if [ -z "$EXOSCALE_API_KEY" ] || [ -z "$EXOSCALE_SECRET_KEY" ]; then
EXOSCALE_API_KEY=""
EXOSCALE_SECRET_KEY=""
_err "You don't specify Exoscale application key and application secret yet."
_err "Please create you key and try again."
_err "EXOSCALE_API_KEY and EXOSCALE_SECRET_KEY must be set."
return 1
fi
_saveaccountconf_mutable EXOSCALE_API_KEY "$EXOSCALE_API_KEY"
_saveaccountconf_mutable EXOSCALE_SECRET_KEY "$EXOSCALE_SECRET_KEY"
return 0
}
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_id=sdjkglgdfewsdfg
# _domain_token=sdjkglgdfewsdfg
_get_root() {
if ! _exoscale_rest GET "domains"; then
return 1
fi
_get_root_domain_id() {
domain=$1
i=2
p=1
i=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
#not valid
return 1
fi
if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
_domain_id=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \")
_domain_token=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
if [ "$_domain_token" ] && [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
candidate=$(printf "%s" "$domain" | cut -d . -f "${i}-100")
[ -z "$candidate" ] && return 1
_debug "Trying root domain candidate: $candidate"
domains=$(_exoscale_rest GET "/dns-domain")
# Extract from dns-domains array
result=$(echo "$domains" | _egrep_o '"dns-domains":\[.*\]' | _egrep_o '\{"id":"[^"]*","created-at":"[^"]*","unicode-name":"[^"]*"\}' | while read -r item; do
name=$(echo "$item" | _egrep_o '"unicode-name":"[^"]*"' | cut -d'"' -f4)
id=$(echo "$item" | _egrep_o '"id":"[^"]*"' | cut -d'"' -f4)
if [ "$name" = "$candidate" ]; then
echo "$id"
break
fi
return 1
done)
if [ -n "$result" ]; then
echo "$result"
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
# returns response
_get_sub_domain() {
fulldomain=$1
root_id=$2
root_info=$(_exoscale_rest GET "/dns-domain/$root_id")
_debug root_info "$root_info"
root_name=$(echo "$root_info" | _egrep_o "\"unicode-name\":\"[^\"]*\"" | cut -d\" -f4)
sub=${fulldomain%%."$root_name"}
if [ "$sub" = "_acme-challenge" ]; then
echo ""
else
# Remove _acme-challenge. prefix to get the actual subdomain
echo "${sub#_acme-challenge.}"
fi
}
_find_record_id() {
root_id=$1
name=$2
records=$(_exoscale_rest GET "/dns-domain/$root_id/record")
# Convert search name to lowercase for case-insensitive matching
name_lower=$(echo "$name" | tr '[:upper:]' '[:lower:]')
echo "$records" | _egrep_o '\{[^}]*"name":"[^"]*"[^}]*\}' | while read -r record; do
record_name=$(echo "$record" | _egrep_o '"name":"[^"]*"' | cut -d'"' -f4)
record_name_lower=$(echo "$record_name" | tr '[:upper:]' '[:lower:]')
if [ "$record_name_lower" = "$name_lower" ]; then
echo "$record" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d'"' -f4
break
fi
done
}
_exoscale_sign() {
k=$1
shift
hex_key=$(printf %b "$k" | _hex_dump | tr -d ' ')
printf %s "$@" | _hmac sha256 "$hex_key"
}
_exoscale_rest() {
method=$1
path="$2"
data="$3"
token="$4"
request_url="$EXOSCALE_API/$path"
_debug "$path"
path=$2
data=$3
url="${EXOSCALE_API}${path}"
expiration=$(_math "$(date +%s)" + 300) # 5m from now
# Build the message with the actual body or empty line
message=$(printf "%s %s\n%s\n\n\n%s" "$method" "/v2$path" "$data" "$expiration")
signature=$(_exoscale_sign "$EXOSCALE_SECRET_KEY" "$message" | _base64)
auth="EXO2-HMAC-SHA256 credential=${EXOSCALE_API_KEY},expires=${expiration},signature=${signature}"
_debug "API request: $method $url"
_debug "Signed message: [$message]"
_debug "Authorization header: [$auth]"
export _H1="Accept: application/json"
if [ "$token" ]; then
export _H2="X-DNS-Domain-Token: $token"
else
export _H2="X-DNS-Token: $EXOSCALE_API_KEY:$EXOSCALE_SECRET_KEY"
fi
export _H2="Authorization: ${auth}"
if [ "$data" ] || [ "$method" = "DELETE" ]; then
export _H3="Content-Type: application/json"
_debug data "$data"
response="$(_post "$data" "$request_url" "" "$method")"
response="$(_post "$data" "$url" "" "$method")"
else
response="$(_get "$request_url" "" "" "$method")"
response="$(_get "$url" "" "" "$method")"
fi
if [ "$?" != "0" ]; then
_err "error $request_url"
# shellcheck disable=SC2181
if [ "$?" -ne 0 ]; then
_err "error $url"
return 1
fi
_debug2 response "$response"
echo "$response"
return 0
}

110
dnsapi/dns_firestorm.sh Normal file
View file

@ -0,0 +1,110 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_firestorm_info='Firestorm.ch
Site: firestorm.ch
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_firestorm
Options:
FST_Key Customer ID
FST_Secret API Secret
FST_Url API URL. Optional. Default "https://api.firestorm.ch/acme-dns".
Issues: github.com/acmesh-official/acme.sh/issues/6839
Author: FireStorm GmbH
'
FST_Url_DEFAULT="https://api.firestorm.ch/acme-dns"
######## Public functions #####################
# Usage: dns_firestorm_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_firestorm_add() {
fulldomain=$1
txtvalue=$2
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
_err "FST_Key and FST_Secret must be set"
_err "Get your API credentials at https://admin.firestorm.ch"
return 1
fi
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
_saveaccountconf_mutable FST_Key "$FST_Key"
_saveaccountconf_mutable FST_Secret "$FST_Secret"
if [ "$FST_Url" != "$FST_Url_DEFAULT" ]; then
_saveaccountconf_mutable FST_Url "$FST_Url"
else
_clearaccountconf_mutable FST_Url
fi
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
_info "Adding TXT record for $fulldomain"
_debug "Subdomain" "$subdomain"
_debug "TXT value" "$txtvalue"
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
response="$(_firestorm_api "update" "$body")"
if _contains "$response" "$txtvalue"; then
_info "TXT record added successfully"
return 0
fi
_err "Failed to add TXT record: $response"
return 1
}
# Usage: dns_firestorm_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_firestorm_rm() {
fulldomain=$1
txtvalue=$2
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
_err "FST_Key and FST_Secret must be set"
return 1
fi
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
_info "Removing TXT record for $fulldomain"
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
response="$(_firestorm_api "remove" "$body")"
if _contains "$response" "removed"; then
_info "TXT record removed"
return 0
fi
_err "Failed to remove TXT record: $response"
return 1
}
#################### Private functions below ##################################
# Escape special characters for safe JSON string interpolation
_json_safe() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
_firestorm_api() {
action=$1
data=$2
export _H1="X-Api-User: $FST_Key"
export _H2="X-Api-Key: $FST_Secret"
export _H3="Content-Type: application/json"
_post "$data" "$FST_Url/$action" "" "POST"
}

View file

@ -305,7 +305,7 @@ _freedns_domain_id() {
fi
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>" |
grep -E "<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...

View file

@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_freemyip
Options:
FREEMYIP_Token API Token
Issues: github.com/acmesh-official/acme.sh/issues/6247
Author: Recolic Keghart <root@recolic.net>, @Giova96
Author: Recolic Keghart <root@recolic.net>, @Giova96, ExtremeFiretop
'
FREEMYIP_DNS_API="https://freemyip.com/update?"
@ -68,22 +68,30 @@ dns_freemyip_rm() {
return $?
}
################ Private functions below ################
################ Private functions below ################
_get_root() {
_fmi_d="$1"
echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev
echo "$_fmi_d" | sed 's/.*\.\([^.]*\.[^.]*\.[^.]*\)$/\1/'
}
# There is random failure while calling freemyip API too fast. This function automatically retry until success.
_freemyip_get_until_ok() {
_fmi_url="$1"
for i in $(seq 1 8); do
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..."
_get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0
_fmi_i=1
while [ "$_fmi_i" -le 8 ]; do
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $_fmi_i/8..."
_fmi_response="$(_get "$_fmi_url")"
printf '%s\n' "$_fmi_response" >&2
if _contains "$_fmi_response" "OK"; then
return 0
fi
_sleep 1 # DO NOT send the request too fast
_fmi_i=$((_fmi_i + 1))
done
_err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'"
_err "Failed to request freemyip API. Server does not say 'OK'"
return 1
}
@ -93,13 +101,16 @@ _is_root_domain_published() {
_webroot="$(_get_root "$_fmi_d")"
_info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet"
for i in $(seq 1 3); do
_debug "'$_webroot' ns lookup, retry $i/3..."
_fmi_i=1
while [ "$_fmi_i" -le 3 ]; do
_debug "'$_webroot' ns lookup, retry $_fmi_i/3..."
if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then
_debug "'$_webroot' already has a TXT record published!"
return 0
fi
_sleep 10 # Give it some time to propagate the TXT record
_fmi_i=$((_fmi_i + 1))
done
return 1
}

View file

@ -23,6 +23,8 @@ dns_gandi_livedns_add() {
fulldomain=$1
txtvalue=$2
GANDI_LIVEDNS_KEY="${GANDI_LIVEDNS_KEY:-$(_readaccountconf_mutable GANDI_LIVEDNS_KEY)}"
GANDI_LIVEDNS_TOKEN="${GANDI_LIVEDNS_TOKEN:-$(_readaccountconf_mutable GANDI_LIVEDNS_TOKEN)}"
if [ -z "$GANDI_LIVEDNS_KEY" ] && [ -z "$GANDI_LIVEDNS_TOKEN" ]; then
_err "No Token or API key (deprecated) specified for Gandi LiveDNS."
_err "Create your token or key and export it as GANDI_LIVEDNS_KEY or GANDI_LIVEDNS_TOKEN respectively"
@ -31,11 +33,11 @@ dns_gandi_livedns_add() {
# Keep only one secret in configuration
if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then
_saveaccountconf GANDI_LIVEDNS_TOKEN "$GANDI_LIVEDNS_TOKEN"
_clearaccountconf GANDI_LIVEDNS_KEY
_saveaccountconf_mutable GANDI_LIVEDNS_TOKEN "$GANDI_LIVEDNS_TOKEN"
_clearaccountconf_mutable GANDI_LIVEDNS_KEY
elif [ -n "$GANDI_LIVEDNS_KEY" ]; then
_saveaccountconf GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY"
_clearaccountconf GANDI_LIVEDNS_TOKEN
_saveaccountconf_mutable GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY"
_clearaccountconf_mutable GANDI_LIVEDNS_TOKEN
fi
_debug "First detect the root zone"

View file

@ -69,7 +69,12 @@ dns_gd_add() {
return 1
fi
if ! _contains "$response" "$txtvalue"; then
if _contains "$response" "UNKNOWN_DOMAIN"; then
# GoDaddy sometimes returns UNKNOWN_DOMAIN when reading a record back even
# though the PUT above succeeded; skip the local readback check and let
# acme.sh's own DNS propagation check verify the record was published.
_info "GoDaddy API won't allow reading the record back; skipping local verification."
elif ! _contains "$response" "$txtvalue"; then
_err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!"
return 1
fi
@ -145,8 +150,8 @@ dns_gd_rm() {
# _domain=domain.com
_get_root() {
domain=$1
i=2
p=1
i=1
p=0
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
@ -154,17 +159,41 @@ _get_root() {
return 1
fi
if ! _gd_rest GET "domains/$h"; then
return 1
# The record name is whatever precedes the candidate zone. Do not assume
# _acme-challenge here: with DNS alias mode it can be any name, and the
# record may even sit at the zone apex (name "@").
if [ "$p" = "0" ]; then
_probe_sub="@"
else
_probe_sub=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
fi
if _contains "$response" '"code":"NOT_FOUND"'; then
_debug "$h not found"
else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
# Probe with the records endpoint instead of "GET domains/$h": since
# 2024-05 GoDaddy rejects the domain details call for accounts with
# fewer than 10 domains, while record-level calls keep working.
# https://github.com/acmesh-official/acme.sh/issues/4487
if ! _gd_rest GET "domains/$h/records/TXT/$_probe_sub"; then
return 1
fi
if _startswith "$response" '\['; then
_sub_domain="$_probe_sub"
_domain="$h"
return 0
fi
# Some accounts get UNKNOWN_DOMAIN when reading records of a valid zone
# even though writes succeed (see issue #6517); fall back to the domain
# details call for them.
if ! _gd_rest GET "domains/$h"; then
return 1
fi
if _contains "$response" '"domainId"'; then
_sub_domain="$_probe_sub"
_domain="$h"
return 0
fi
_debug "$h not found"
p="$i"
i=$(_math "$i" + 1)
done

263
dnsapi/dns_glesys.sh Normal file
View file

@ -0,0 +1,263 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_glesys_info='Glesys
Site: Glesys.se
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_glesys
Options:
GLESYS_API_KEY Generated API key.
GLESYS_PROJECT_ID Project ID for the API key (e.g. cl12345).
GLESYS_API API endpoint. Default "https://api.glesys.com/domain".
GLESYS_TTL TXT record TTL. Default 120.
Issues: https://github.com/acmesh-official/acme.sh/issues/7057
Author: Toni Karppi
'
GLESYS_API_DEFAULT="https://api.glesys.com/domain"
GLESYS_TTL_DEFAULT="120"
######## Public functions #####################################################
# Usage:
# dns_glesys_add _acme-challenge.www.example.com "txt-value"
dns_glesys_add() {
fulldomain="$1"
txtvalue="$2"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
_glesys_init || return 1
if ! _glesys_get_root "$fulldomain"; then
_err "Could not find root zone for $fulldomain"
return 1
fi
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
host_value="${_sub_domain:-@}"
_debug _host_value "$host_value"
data="{\"domainname\":\"$_domain\",\"host\":\"$host_value\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":\"$GLESYS_TTL\"}"
_debug2 data "$data"
if ! _glesys_rest POST "/addrecord" "$data"; then
_err "Failed to send HTTP request to add TXT record"
return 1
fi
response_code=$(
printf "%s" "$response" |
tr -d '\r\n\t ' |
_egrep_o '"code":"?[0-9]+' |
_egrep_o '[0-9]+$'
)
_debug response_code "$response_code"
if [ "$response_code" != "200" ]; then
_err "GleSYS API responded with an unexpected status when attempting to add TXT record"
_debug2 "API response" "$response"
return 1
fi
_info "TXT record added"
return 0
}
# Usage:
# dns_glesys_rm _acme-challenge.www.example.com "txt-value"
dns_glesys_rm() {
fulldomain="$1"
txtvalue="$2"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
_glesys_init || return 1
if ! _glesys_get_root "$fulldomain"; then
_err "Could not find root zone for $fulldomain"
return 1
fi
if ! _glesys_find_record_id "$txtvalue"; then
_info "TXT record not present, skip removal"
return 0
fi
_debug _record_id "$_record_id"
if ! _glesys_rest POST "/deleterecord" "{\"recordid\":$_record_id}"; then
_err "Failed to send HTTP request to remove TXT record"
return 1
fi
response_code=$(
printf "%s" "$response" |
tr -d '\r\n\t ' |
_egrep_o '"code":"?[0-9]+' |
_egrep_o '[0-9]+$'
)
_debug response_code "$response_code"
if [ "$response_code" != "200" ]; then
_err "GleSYS API responded with unexpected status when attempting to remove TXT record"
_debug2 "API response" "$response"
return 1
fi
_info "TXT record removed"
return 0
}
######## Private functions ####################################################
_glesys_find_record_id() {
txtvalue="$1"
_debug txtvalue "$txtvalue"
if [ -z "$txtvalue" ]; then
return 1
fi
_record_id=""
_debug "Looking for TXT record with value" "$txtvalue"
if ! _glesys_rest GET "/listrecords?domainname=$_domain"; then
_err "Failed to list DNS records"
return 1
fi
records="$(
printf "%s" "$response" |
tr -d '\r\n\t ' |
sed 's/},{/}\
{/g'
)"
_debug2 records "$records"
expected_data="\"data\":\"$txtvalue\""
_record_id="$(
printf "%s\n" "$records" |
while IFS= read -r record; do
printf "%s" "$record" | grep -q '"type":"TXT"' || continue
printf "%s" "$record" | grep -Fq "$expected_data" || continue
printf "%s" "$record" |
grep -E -o '"recordid":"?[0-9]+' |
grep -E -o '[0-9]+$'
break
done
)"
_debug _record_id "$_record_id"
if [ -z "$_record_id" ]; then
return 1
fi
return 0
}
# Finds:
# _domain example.com
# _sub_domain _acme-challenge.www
_glesys_get_root() {
domain="$1"
i=1
while true; do
h="$(printf "%s" "$domain" | cut -d . -f "$i"-100)"
if [ -z "$h" ]; then
return 1
fi
if _glesys_rest GET "/listrecords?domainname=$h"; then
response_code=$(
printf "%s" "$response" |
tr -d '\r\n\t ' |
_egrep_o '"code":"?[0-9]+' |
_egrep_o '[0-9]+$'
)
_debug response_code "$response_code"
if [ "$response_code" = "200" ]; then
cut_len="$((${#domain} - ${#h} - 1))"
_domain="$h"
_sub_domain="$(printf "%s" "$domain" | cut -c "1-$cut_len")"
return 0
fi
fi
i="$((i + 1))"
done
}
_glesys_init() {
[ -z "$GLESYS_API" ] && GLESYS_API="$GLESYS_API_DEFAULT"
[ -z "$GLESYS_TTL" ] && GLESYS_TTL="$GLESYS_TTL_DEFAULT"
_debug GLESYS_API "$GLESYS_API"
_debug GLESYS_TTL "$GLESYS_TTL"
GLESYS_API_KEY="${GLESYS_API_KEY:-$(_readaccountconf_mutable GLESYS_API_KEY)}"
GLESYS_PROJECT_ID="${GLESYS_PROJECT_ID:-$(_readaccountconf_mutable GLESYS_PROJECT_ID)}"
if [ -z "$GLESYS_API_KEY" ] || [ -z "$GLESYS_PROJECT_ID" ]; then
_err "GLESYS_API_KEY and GLESYS_PROJECT_ID must be set for this provider"
return 1
fi
_secure_debug GLESYS_API_KEY "$GLESYS_API_KEY"
_secure_debug GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID"
_glesys_basic_auth="$(printf "%s:%s" "$GLESYS_PROJECT_ID" "$GLESYS_API_KEY" | _base64)"
_secure_debug2 _glesys_basic_auth "$_glesys_basic_auth"
_saveaccountconf_mutable GLESYS_API_KEY "$GLESYS_API_KEY"
_saveaccountconf_mutable GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID"
return 0
}
_glesys_rest() {
method="$1"
path="$2"
data="$3"
export _H1="Authorization: Basic $_glesys_basic_auth"
export _H2="Content-Type: application/json"
export _H3="Accept: application/json"
url="$GLESYS_API$path"
_debug "$method $url"
if [ "$method" = "GET" ]; then
response="$(_get "$url")"
else
response="$(_post "$data" "$url" "" "$method")"
fi
ret="$?"
_debug2 response "$response"
_debug ret "$ret"
if [ "$ret" != "0" ]; then
return 1
fi
return 0
}

303
dnsapi/dns_gname.sh Normal file
View file

@ -0,0 +1,303 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_gname_info='GNAME
Site: www.gname.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gname
Options:
GNAME_APPID Your APPID
GNAME_APPKEY Your APPKEY
GNAME_TTL DNS resolution record TTL value, default 120.
Issues: github.com/acmesh-official/acme.sh/issues/6874
Author: GNDevProd <tech@gname.com>
'
GNAME_TLD_Api="https://www.gname.com/request/tlds?lx=all"
GNAME_Api="https://api.gname.com"
GNAME_TLDS_CACHE=""
######## Public functions #####################
#Usage: add _acme-challenge.www.domain.com "T1rxqRBosdIK90xWCG3KLZNf6q_0HG9i01zxXp5CAS3"
dns_gname_add() {
fulldomain=$1
txtvalue=$(printf "%s" "$2" | _url_encode)
#Compatible with gname API RFC 1738 standard URL encoding
txtvalue=$(printf '%s' "$txtvalue" | sed 's/%20/+/g')
GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}"
GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}"
GNAME_TTL="${GNAME_TTL:-$(_readaccountconf_mutable GNAME_TTL)}"
GNAME_TTL="${GNAME_TTL:-120}"
if [ -z "$GNAME_APPID" ] || [ -z "$GNAME_APPKEY" ]; then
GNAME_APPID=""
GNAME_APPKEY=""
_err "You have not configured the APPID and APPKEY for the GNAME API."
_err "You can get yours from here https://www.gname.com/domain/api."
return 1
fi
_saveaccountconf_mutable GNAME_APPID "$GNAME_APPID"
_saveaccountconf_mutable GNAME_APPKEY "$GNAME_APPKEY"
_saveaccountconf_mutable GNAME_TTL "$GNAME_TTL"
if ! _extract_domain "$fulldomain"; then
_err "Failed to extract domain. Please check your network or API response."
return 1
fi
gntime=$(date +%s)
#If the hostname is empty, you need to replace it with @.
final_hostname=$(printf "%s" "${ext_hostname:-@}" | _url_encode)
# Parameters need to be sorted by key
body="appid=$GNAME_APPID&exist=1&gntime=$gntime&jlz=$txtvalue&lang=us&lx=TXT&mx=0&ttl=$GNAME_TTL&xl=0&ym=$ext_domain&zj=$final_hostname"
_info "Adding TXT record for $ext_domain, host: $final_hostname"
if _post_to_api "/api/resolution/add" "$body"; then
_info "Successfully added DNS record."
return 0
else
_err "Failed to add DNS record via Gname API."
return 1
fi
}
#Usage: remove _acme-challenge.www.domain.com "T1rxqRBosdIK90xWCG3KLZNf6q_0HG9i01zxXp5CASc"
dns_gname_rm() {
fulldomain=$1
txtvalue=$2
GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}"
GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}"
if [ -z "$GNAME_APPID" ] || [ -z "$GNAME_APPKEY" ]; then
GNAME_APPID=""
GNAME_APPKEY=""
_err "You have not configured the APPID and APPKEY for the GNAME API."
_err "You can get yours from here https://www.gname.com/domain/api."
return 1
fi
_saveaccountconf_mutable GNAME_APPID "$GNAME_APPID"
_saveaccountconf_mutable GNAME_APPKEY "$GNAME_APPKEY"
if ! _extract_domain "$fulldomain"; then
_err "Failed to extract domain. Please check your network or API response."
return 1
fi
final_hostname="${ext_hostname:-@}"
_debug "Query DNS record ID $ext_domain $final_hostname $txtvalue"
if ! record_id=$(_get_record_id "$ext_domain" "$final_hostname" "$txtvalue"); then
_err "Error occurred during record lookup. Skipping deletion to avoid errors."
return 1
fi
if [ -z "$record_id" ]; then
_info "DNS record not found, skip removing."
return 0
fi
_debug "DNS record ID:$record_id"
gntime=$(date +%s)
body="appid=$GNAME_APPID&gntime=$gntime&jxid=$record_id&lang=us&ym=$ext_domain"
if ! _post_to_api "/api/resolution/delete" "$body"; then
_err "DNS record deletion failed"
return 1
fi
_info "DNS record deletion successful"
return 0
}
# Find the DNS record ID by hostname, record type, and record value.
_get_record_id() {
target_ym="$1"
target_zjt="$2"
target_jxz="$3"
target_lx="TXT"
GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}"
GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}"
gntime=$(date +%s)
body="appid=$GNAME_APPID&gntime=$gntime&limit=1000&lx=$target_lx&page=1&ym=$target_ym"
if ! _post_to_api "/api/resolution/list" "$body"; then
_err "Query and parsing records failed"
return 1
fi
clean_response=$(echo "$post_response" | tr -d '\r')
records=$(echo "$clean_response" | sed 's/.*"data":\[//; s/\],"count".*//; s/},/}\n/g' | grep "^{")
matched_rows=$(echo "$records" | grep -Fi "\"zjt\":\"$target_zjt\"")
if [ -z "$matched_rows" ]; then
_debug "No records found for host: $target_zjt"
return 0
fi
exact_row=$(echo "$matched_rows" | grep -F "\"jxz\":\"$target_jxz\"" | _head_n 1)
dns_record_id=""
if [ -n "$exact_row" ]; then
dns_record_id=$(echo "$exact_row" | _egrep_o "\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d '"')
fi
if [ -n "$dns_record_id" ]; then
_debug "Successfully found exact record ID: $dns_record_id"
printf "%s" "$dns_record_id"
return 0
fi
_debug "Can not find exact DNS record match for: $target_zjt"
return 0
}
# Request GNAME API,post_response: Response content
_post_to_api() {
uri=$1
body=$2
url="$GNAME_Api$uri"
gntoken=$(_gntoken "$body")
body="$body&gntoken=$gntoken"
post_response="$(_post "$body" "$url" "" "POST" "application/x-www-form-urlencoded")"
http_err_code=$?
if [ "$http_err_code" != "0" ]; then
_err "POST API $url request failed:$http_err_code"
return 1
fi
normalized_response="$(echo "$post_response" | _normalizeJson)"
if [ -z "$normalized_response" ]; then
_err "Failed to normalize JSON response for [$uri]"
return 1
fi
ret_code=$(echo "$normalized_response" | sed 's/.*"code":\([-0-9]*\).*/\1/')
if [ "$ret_code" = "1" ]; then
return 0
fi
if [ "$uri" = "/api/resolution/add" ]; then
if _contains "$normalized_response" "the same host records and record values"; then
_info "DNS record already exists, treat as success."
return 0
fi
fi
ret_msg=$(echo "$normalized_response" | sed 's/.*"msg":"\([^"]*\)".*/\1/')
_err "POST API $url error: [$ret_code] $ret_msg"
_debug "Full response: $normalized_response"
return 1
}
# Split the complete domain into a host and a main domain.
# example, www.gname.com can be split into ext_hostname=www,ext_domain=gname.com
_extract_domain() {
host="$1"
# Prioritize reading from the cache and reduce network caching
if [ -z "$GNAME_TLDS_CACHE" ]; then
GNAME_TLDS_CACHE=$(_get_suffixes_json)
fi
if [ -z "$GNAME_TLDS_CACHE" ]; then
_err "The list of domain suffixes is empty after retrieval; cannot extract domain"
return 1
fi
main_part=$(echo "$GNAME_TLDS_CACHE" | sed 's/.*"main":\[\([^]]*\)\].*/\1/' | tr -d '"' | tr ',' ' ')
sub_part=$(echo "$GNAME_TLDS_CACHE" | sed 's/.*"sub":\[\([^]]*\)\].*/\1/' | tr -d '"' | tr ',' ' ')
suffix_list=$(echo "$main_part $sub_part" | tr -s ' ' | sed 's/^[ ]//;s/[ ]$//')
dot_count=$(echo "$host" | _egrep_o "\." | wc -l)
if [ "$dot_count" -eq 0 ]; then
_err "Invalid domain format: $host (missing dot)"
return 1
fi
if [ "$dot_count" -eq 1 ]; then
ext_hostname=""
ext_domain="$host"
elif [ "$dot_count" -gt 1 ]; then
matched_suffix=""
for suffix in $suffix_list; do
case "$host" in
*".$suffix")
if [ -z "$matched_suffix" ] || [ "${#suffix}" -gt "${#matched_suffix}" ]; then
matched_suffix="$suffix"
fi
;;
esac
done
if [ -n "$matched_suffix" ]; then
prefix="${host%."$matched_suffix"}"
main_name="${prefix##*.}"
ext_domain="$main_name.$matched_suffix"
else
_tld="${host##*.}"
_tmp="${host%.*}"
_main="${_tmp##*.}"
ext_domain="$_main.$_tld"
fi
if [ "$host" = "$ext_domain" ]; then
ext_hostname=""
else
ext_hostname="${host%."$ext_domain"}"
fi
fi
_debug "ext_hostname:$ext_hostname"
_debug "ext_domain:$ext_domain"
return 0
}
# Obtain the list of domain suffixes via API
_get_suffixes_json() {
_debug "GET request URL: $GNAME_TLD_Api Retrieves a list of domain suffixes."
if ! response="$(_get "$GNAME_TLD_Api")"; then
_err "Failed to retrieve list of domain suffixes"
return 1
fi
if [ -z "$response" ]; then
_err "The list of domain suffixes is empty"
return 1
fi
normalized_response="$(echo "$response" | _normalizeJson)"
if [ -z "$normalized_response" ]; then
_err "Failed to normalize JSON response for domain suffix list"
return 1
fi
if ! _contains "$normalized_response" "\"code\":1"; then
_err "Failed to retrieve list of domain name suffixes; code is not 1"
return 1
fi
echo "$normalized_response"
return 0
}
# Generate API authentication signature
_gntoken() {
data_to_sign="$1"
full_data="${data_to_sign}${GNAME_APPKEY}"
hash=$(printf "%s" "$full_data" | _digest md5 hex | tr -d ' ')
hash_upper=$(echo "$hash" | _upper_case)
printf "%s" "$hash_upper"
}

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