Compare commits

...

65 Commits
3.1.4 ... dev

Author SHA1 Message Date
neil
7b5a902755 Let an explicit --days or --valid-to outrank the ARI window
ARI has overridden Le_NextRenewTime unconditionally since 3.1.4, so a user
who passed --days never got the schedule they asked for, and --valid-to was
guarded at issue time but not on the renewal check: the guard survived one
run before the next cron rewrote it and saved it back.

An explicit --days or --valid-to now pins the schedule. The window is still
taken when it is earlier than what the user asked for, so a CA can pull an
urgent renewal forward but can never push a pinned renewal back.
Le_RenewalDays is only written to the domain conf when --days was actually
passed, so its presence there is what marks a schedule as pinned.

A fixed-date --valid-to opts out of ARI entirely: that cert is not renewed
automatically at all, so pulling it forward would change what it does, not
just when it renews.

Both call sites go through the new _calc_ari_renew_time.
2026-09-01 20:26:34 +08:00
neil
4a3f8b5dea Wait for the order to be ready instead of failing the finalize 2026-09-01 18:59:56 +08:00
neil
08ac83459d Treat alreadyRevoked as a successful revocation 2026-08-31 09:24:41 +08:00
neil
2b7487ba72 Back off between retries instead of a flat two seconds 2026-08-30 21:24:02 +08:00
neil
19efdf269a Retry a gateway error from the CA instead of failing the order 2026-08-30 20:47:24 +08:00
neil
cdca555cad Add OPNsense and HardenedBSD to CI 2026-08-30 17:46:48 +08:00
neil
cce4a28b99 Fix the standalone port check on the bsds, macos and haiku 2026-08-30 17:39:57 +08:00
neil
4756183e30 Never truncate the conf file when a saved value breaks the rewrite sed
A value holding a backslash-digit sequence (a backreference to sed) or an
embedded line break made _setopt's replace command fail after the shell
had already truncated the conf file, wiping the whole domain conf; the
next renewal then fails with an empty Le_API and no validation method.
Same class as #2426, which escaped only '&' and '|'.

Escape the backslash too, write the sed output back only when sed
succeeds, reject values holding a line break, and rewrite the file with
printf instead of echo in the append path and in _clear_conf: dash's
builtin echo interprets backslash escapes and corrupted such values on
every rewrite.

https://github.com/acmesh-official/acme.sh/issues/7213
2026-08-30 16:02:57 +08:00
neil
6e1deacac0 dns_azure: never read or persist AZUREDNS_BEARERTOKEN from account.conf
Versions up to 3.0.9 cached the internally-acquired access token as
SAVED_AZUREDNS_BEARERTOKEN. 3.1.0 repurposed that variable for
user-supplied bearer tokens, so after an upgrade the stale cached token
was read back as if user-supplied, skipped the refresh path, and failed
renewals with 401 forever once expired.

A bearer token is short-lived, so persisting it is never useful: take it
from the environment only, and clear any stale saved value on the next
run.

fix https://github.com/acmesh-official/acme.sh/issues/7218
2026-08-30 15:42:27 +08:00
skysaint
c6cd844986 dns_jd: upgrade to JD Cloud v2 API (#7207) 2026-08-30 15:33:32 +08:00
wurzelpanzer
ef7b2d3c2e dns_easydns: match the TXT record by its rdata when removing (#7199)
dns_easydns_rm() picked the first id in the search response and ignored
$txtvalue. When two challenge records exist under the same host - for
example when example.com and *.example.com are issued as separate
certificates - a concurrent run's record could be deleted instead of
our own.

Select the record by its rdata instead, following the dns_cf.sh
convention of matching name + value. tr '{' '\n' puts one record per
line, so both _egrep_o branches - egrep -o and the BRE sed fallback -
return the same single id. Without it the sed fallback would return
only the last match, since .* is greedy.

An empty record_id is now treated as "nothing to remove" and returns 0,
rather than being reported as an error.

Also add the credential check that _rm was missing. It deliberately
does not call _saveaccountconf_mutable, as _add already does that.

Co-authored-by: wurzelpanzer <wurzelpanzer@maximolider.net>
2026-08-22 10:15:52 +08:00
Fabian Lesniak
b481ffb81b Merge pull request #5194 from flesniak/myloc
Add dnsapi script for myloc.de/webtropia.com
2026-08-22 10:13:58 +08:00
neil
b1a8eb1c95 installcronjob: match the stale cron path without grep -F 2026-08-20 08:40:49 +08:00
neil
e0c0297ba3 fix cronjob 2026-08-19 22:50:03 +08:00
neil
1cbd223386 deploy/unifios: document UniFi OS hardware support, not just self-hosted
The certificate REST API this hook drives is UniFi OS's own, not
specific to the self-hosted UniFi OS Server: user reports confirm it on
a UDM Pro (UniFi OS 5.1.26) and a UCG Fiber (5.0.16). Reframe the scope
around the endpoint rather than the product line, state that the choice
between unifi and unifios is local/SSH file access vs remote REST API,
and note that the management port is 11443 on UniFi OS Server but 443
on hardware, so DEPLOY_UNIFIOS_HOST must be set there.
2026-08-17 13:42:10 +08:00
neil
5180911929 fix https://github.com/acmesh-official/acme.sh/issues/7195#issuecomment-5281002963 2026-08-14 12:15:15 +08:00
neil
5ff7f0a4e7 Mirror the tag object, not the commit, in vtag.yml
The v-prefixed mirror was created from github.sha, so for an annotated or
signed tag it would point at the commit and drop the signature: "git
verify-tag v3.1.3" fails with "cannot verify a non-tag object of type
commit" while "git verify-tag 3.1.3" succeeds. Resolve refs/tags/<tag>
and mirror whatever object it points at instead, which keeps the current
behaviour for lightweight tags. Also move the workflow expressions into
env instead of interpolating them into the shell command.
2026-08-14 11:53:43 +08:00
Pablo
41bdd4cd0e Add UniFi OS Server deploy hook (#7184)
* 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
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
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)
* 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
_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
_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)
* 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 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
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) 2026-07-24 13:55:17 +08:00
Radu
24d573b6d3 Merge pull request #7140 from radumalica/dns_hestiacp
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
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
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 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
59a97d7f8b fix bug for solaris.
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
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
7cc16cd09a Merge pull request #7126 from acmesh-official/dev
sync
2026-07-14 22:35:07 +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
3e64b5e6f5 Merge pull request #7119 from acmesh-official/dev
sync
2026-07-12 10:11:32 +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
1651a5a609 Merge pull request #7095 from acmesh-official/dev
sync
2026-07-06 16:37:03 +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
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
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
9f0ef7abcd Merge pull request #7090 from acmesh-official/dev
sync
2026-07-06 14:28:08 +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
f50401a342 Merge pull request #7086 from acmesh-official/dev
sync
2026-07-06 10:37:29 +08:00
neil
992886c4eb Merge pull request #7078 from acmesh-official/dev
sync
2026-07-03 19:56:09 +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
neil
3c848adbb7 Merge pull request #7008 from acmesh-official/dev
sync
2026-06-04 22:50:25 +02:00
neil
f265c304da Merge pull request #6968 from acmesh-official/dev
sync
2026-05-16 11:17:24 +02:00
56 changed files with 2945 additions and 496 deletions

View File

@@ -66,7 +66,7 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Set env file - name: Set env file
@@ -114,7 +114,7 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Install tools - name: Install tools
run: | run: |
brew untap aws/tap || true brew untap aws/tap || true
@@ -167,7 +167,7 @@ jobs:
- name: Set git to use LF - name: Set git to use LF
run: | run: |
git config --global core.autocrlf false git config --global core.autocrlf false
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Install cygwin base packages with chocolatey - name: Install cygwin base packages with chocolatey
run: | run: |
choco config get cacheLocation choco config get cacheLocation
@@ -231,12 +231,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/freebsd-vm@v1 - uses: vmactions/freebsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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 prepare: pkg install -y socat curl
usesh: true usesh: true
@@ -289,12 +290,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/ghostbsd-vm@v1 - uses: vmactions/ghostbsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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 prepare: pkg install -y socat curl
usesh: true usesh: true
@@ -345,12 +347,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh 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/openbsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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_add socat curl libiconv
usesh: true usesh: true
@@ -401,12 +404,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/netbsd-vm@v1 - uses: vmactions/netbsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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: | prepare: |
/usr/sbin/pkg_add curl socat /usr/sbin/pkg_add curl socat
@@ -458,12 +462,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/dragonflybsd-vm@v1 - uses: vmactions/dragonflybsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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: | prepare: |
pkg install -y libnghttp2 pkg install -y libnghttp2
@@ -519,12 +524,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/midnightbsd-vm@v1 - uses: vmactions/midnightbsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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 prepare: mport install socat curl || true
usesh: true usesh: true
@@ -576,12 +582,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/solaris-vm@v1 - uses: vmactions/solaris-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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 sync: nfs
prepare: | prepare: |
@@ -635,12 +642,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/omnios-vm@v1 - uses: vmactions/omnios-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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 sync: nfs
prepare: pkg install socat prepare: pkg install socat
@@ -691,12 +699,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openindiana-vm@v1 - uses: vmactions/openindiana-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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 sync: nfs
prepare: pkg install socat prepare: pkg install socat
@@ -747,12 +756,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/tribblix-vm@v1 - uses: vmactions/tribblix-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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 sync: nfs
prepare: zap install socat prepare: zap install socat
@@ -803,19 +813,20 @@ jobs:
TokenName4: ${{ secrets.TokenName4}} TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}} TokenName5: ${{ secrets.TokenName5}}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Clone acmetest - name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/haiku-vm@v1 - uses: vmactions/haiku-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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}}' 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 sync: rsync
copyback: false copyback: false
prepare: | prepare: |
mkdir -p /boot/home/.cache mkdir -p /boot/home/.cache
pkgman install -y cronie pkgman install -y cronie
run: | run: |
if [ "${{ secrets.TokenName1}}" ] ; then if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@@ -842,3 +853,236 @@ jobs:
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"
HardenedBSD:
runs-on: ubuntu-latest
needs: OpenEuler
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/hardenedbsd-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
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"
OPNsense:
runs-on: ubuntu-latest
needs: HardenedBSD
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/opnsense-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}}'
#The dns-01 cases need no inbound port, so the appliance's web GUI can
#keep the 80 port here, unlike the standalone workflow.
prepare: pkg install -y socat curl
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"

View File

@@ -58,6 +58,7 @@ jobs:
- uses: vmactions/dragonflybsd-vm@v1 - uses: vmactions/dragonflybsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

View File

@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/freebsd-vm@v1 - uses: vmactions/freebsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

View File

@@ -66,6 +66,7 @@ jobs:
- uses: vmactions/ghostbsd-vm@v1 - uses: vmactions/ghostbsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

View File

@@ -65,6 +65,7 @@ jobs:
- uses: vmactions/haiku-vm@v1 - uses: vmactions/haiku-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

76
.github/workflows/HardenedBSD.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: HardenedBSD
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/HardenedBSD.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/HardenedBSD.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
HardenedBSD:
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
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@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/hardenedbsd-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"

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

@@ -58,6 +58,7 @@ jobs:
- uses: vmactions/midnightbsd-vm@v1 - uses: vmactions/midnightbsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

View File

@@ -58,6 +58,7 @@ jobs:
- uses: vmactions/netbsd-vm@v1 - uses: vmactions/netbsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

86
.github/workflows/OPNsense.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
name: OPNsense
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/OPNsense.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/OPNsense.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
OPNsense:
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
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@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/opnsense-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: |
#OPNsense is a firewall appliance whose web GUI holds the 80 port,
#where every --standalone case listens. configd has no "stop"
#action for it and the rc script cannot stop it either, so kill it.
#This belongs here and not in prepare: prepare runs before the
#cache-after-prepare reboot, which would bring the GUI back. And do
#NOT free the port by disabling the GUI's http redirect in
#config.xml: pf's automatic pass rule for the 80 port is generated
#from the web GUI settings, so dropping the redirect also drops the
#rule on the next boot, and the inbound challenge is filtered.
pkill lighttpd || true
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

@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/omnios-vm@v1 - uses: vmactions/omnios-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

View File

@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/openbsd-vm@v1 - uses: vmactions/openbsd-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

78
.github/workflows/OpenEuler.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
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: |
# openEuler ships every repo with both a baseurl and a metalink.
# The metalink mirror pool is split-brain (dl-cdn.openeuler.openatom.cn
# froze at the 2026-08-20 snapshot while repo.openeuler.org moved on),
# so dnf takes repomd.xml from the stale mirror and then 404s fetching
# the checksummed metadata it names from the fresh ones. Keep only the
# vendor baseurl, which is self-consistent.
sed -i '/^metalink=/d' /etc/yum.repos.d/*.repo
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"

View File

@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/openindiana-vm@v1 - uses: vmactions/openindiana-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

View File

@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/solaris-vm@v1 - uses: vmactions/solaris-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

View File

@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/tribblix-vm@v1 - uses: vmactions/tribblix-vm@v1
with: with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }} 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' envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: | nat: |
"8080": "80" "8080": "80"

44
.github/workflows/vtag.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
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 }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
if gh api "repos/$REPO/git/ref/tags/v$TAG" >/dev/null 2>&1; then
echo "Tag v$TAG already exists, nothing to do."
exit 0
fi
# Mirror the object the pushed tag actually points at: the commit
# for a lightweight tag, the tag object itself for an annotated or
# signed one. Pointing the mirror at the commit would strip the
# signature, so "git verify-tag v3.1.3" would fail while
# "git verify-tag 3.1.3" succeeds.
sha="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq .object.sha)"
if [ -z "$sha" ] || [ "$sha" = "null" ]; then
echo "Could not resolve refs/tags/$TAG"
exit 1
fi
gh api "repos/$REPO/git/refs" -f ref="refs/tags/v$TAG" -f sha="$sha"
echo "Created tag v$TAG -> $sha"

View File

@@ -36,6 +36,10 @@
<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/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/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/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>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml/badge.svg" alt="HardenedBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml/badge.svg" alt="OPNsense"></a>
</p> </p>
<p align="center"> <p align="center">
@@ -130,6 +134,10 @@
|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 |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 |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 |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
|30|[![HardenedBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml)|HardenedBSD
|31|[![OPNsense](https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml)|OPNsense
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest) > 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
@@ -223,6 +231,31 @@ Cron entry example:
acme.sh -h acme.sh -h
``` ```
#### 🔏 Verify a Release
Release tags from `3.1.5` on are signed with the maintainer's SSH key. The
signing happens on the maintainer's machine, so the private key is never
available to CI. The public half is [`allowed_signers`](allowed_signers) in
this repository. From a clone:
```bash
git config gpg.ssh.allowedSignersFile allowed_signers
```
```bash
git verify-tag 3.1.5
```
The signature covers the tag object, which pins the commit and therefore the
whole tree, so a good signature verifies every file at that release and no
separate tarball checksum is needed. Build a tarball from the verified tag:
```bash
git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz
```
> ⚠️ Tags up to `3.1.4` predate the signing key and are unsigned.
--- ---
### 2⃣ Issue a Certificate ### 2⃣ Issue a Certificate

654
acme.sh

File diff suppressed because it is too large Load Diff

20
allowed_signers Normal file
View File

@@ -0,0 +1,20 @@
# acme.sh release signing key.
#
# Release tags are signed with this key. Its private half is held by the
# maintainer and is never available to CI, so a compromise of the build
# pipeline cannot produce a tag that verifies against this file.
#
# Fingerprint: SHA256:M60qVafm/NUywQHXAkoQcj2v6KgkfrdSXv6mPejUUeE
#
# To verify a release tag, from a clone of this repository:
#
# git config gpg.ssh.allowedSignersFile allowed_signers
# git verify-tag 3.1.5
#
# A good signature covers the tag object, which pins the commit, which pins
# the whole tree -- so verifying the tag verifies every file at that
# release. Build a tarball from the verified tag with:
#
# git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz
#
github@neilpang.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTjI0HBJn3uhfT2DsNcFybfAZi3ADbIacMpz1BItKdB

View File

@@ -163,8 +163,8 @@ byteplus_alb_deploy() {
# ── 3. Read cert and key ───────────────────────────────────────────────────── # ── 3. Read cert and key ─────────────────────────────────────────────────────
# BytePlus requires NO blank lines between PEM blocks in the certificate chain # BytePlus requires NO blank lines between PEM blocks in the certificate chain
_public_key=$(sed '/^[[:space:]]*$/d' "$_cfullchain" | tr -d '\r') _public_key=$(_strip_blank_lines <"$_cfullchain" | tr -d '\r')
_private_key=$(sed '/^[[:space:]]*$/d' "$_ckey" | tr -d '\r') _private_key=$(_strip_blank_lines <"$_ckey" | tr -d '\r')
if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then
_err "Failed to read certificate or key file." _err "Failed to read certificate or key file."

View File

@@ -87,7 +87,11 @@ cpanel_uapi_deploy() {
# Auto mode # Auto mode
if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then
# call API for site config # 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 # exit if error in response
if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
_err "Error in deploying certificate - cannot retrieve sitelist:" _err "Error in deploying certificate - cannot retrieve sitelist:"

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

@@ -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" _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" _H1="Cookie: $_cookie"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST") _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" _info "_message" "$_message"
if [ -z "$_message" ]; then if [ -z "$_message" ]; then
_err "Fail to upload certificate." _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" _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") _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" _info "_message" "$_message"
if [ -z "$_message" ]; then if [ -z "$_message" ]; then
_err "Fail to apply certificate." _err "Fail to apply certificate."

View File

@@ -10,6 +10,10 @@
# Usage (shown values are the examples): # Usage (shown values are the examples):
# 1. Set optional environment variables # 1. Set optional environment variables
# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set" # - 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: # 2. Run command:
# acme.sh --deploy --deploy-hook multideploy -d example.com # acme.sh --deploy --deploy-hook multideploy -d example.com
@@ -49,7 +53,7 @@ multideploy_deploy() {
_debug _cfullchain "$_cfullchain" _debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx" _debug _cpfx "$_cpfx"
MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}" _getdeployconf MULTIDEPLOY_FILENAME
if [ -z "$MULTIDEPLOY_FILENAME" ]; then if [ -z "$MULTIDEPLOY_FILENAME" ]; then
MULTIDEPLOY_FILENAME="multideploy.yml" MULTIDEPLOY_FILENAME="multideploy.yml"
_info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'." _info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'."
@@ -75,7 +79,8 @@ multideploy_deploy() {
# This function preprocesses the deploy file by checking if 'yq' is installed, # 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. # verifying the existence of the deploy file, and ensuring only one deploy file is present.
# Arguments: # Arguments:
# $@ - Posible deploy file names. # $@ - Posible deploy file names. A name starting with '/' is treated as an
# absolute path, any other name is relative to the domain directory.
# Usage: # Usage:
# _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?" # _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?"
_preprocess_deployfile() { _preprocess_deployfile() {
@@ -87,15 +92,21 @@ _preprocess_deployfile() {
_debug3 "yq is installed." _debug3 "yq is installed."
# Check if deploy file exists # Check if deploy file exists
found_file=""
for file in "$@"; do for file in "$@"; do
_debug3 "Checking file" "$DOMAIN_PATH/$file" if _startswith "$file" "/"; then
if [ -f "$DOMAIN_PATH/$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" _debug3 "File found"
if [ -n "$found_file" ]; then if [ -n "$found_file" ]; then
_err "Multiple deploy files found. Please keep only one deploy file." _err "Multiple deploy files found. Please keep only one deploy file."
return 1 return 1
fi fi
found_file="$file" found_file="$_multideploy_path"
else else
_debug3 "File not found" _debug3 "File not found"
fi fi
@@ -105,12 +116,12 @@ _preprocess_deployfile() {
_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." _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 return 1
fi fi
if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then if ! _check_deployfile "$found_file"; then
_err "Deploy file is not valid: $DOMAIN_PATH/$found_file" _err "Deploy file is not valid: $found_file"
return 1 return 1
fi fi
echo "$DOMAIN_PATH/$found_file" echo "$found_file"
} }
# Description: # Description:

View File

@@ -116,17 +116,24 @@ HEREDOC
export HTTPS_INSECURE=1 export HTTPS_INSECURE=1
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}" export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json") response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
# 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)" response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"') message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
_retval=$? case "$_status_code" in
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then 2[0-9][0-9])
_debug3 response "$response" if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_info "Certificate successfully deployed" _debug3 response "$response"
return 0 _info "Certificate successfully deployed"
else return 0
_err "Certificate deployment failed: $message" fi
_debug "Response" "$response" ;;
return 1 esac
fi _err "Certificate deployment failed (HTTP status $_status_code). $message"
_debug "Response" "$response"
return 1
} }

View File

@@ -128,17 +128,24 @@ HEREDOC
export HTTPS_INSECURE=1 export HTTPS_INSECURE=1
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}" export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json") response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
# 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)" response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"') message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
_retval=$? case "$_status_code" in
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then 2[0-9][0-9])
_debug3 response "$response" if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_info "Certificate successfully deployed" _debug3 response "$response"
return 0 _info "Certificate successfully deployed"
else return 0
_err "Certificate deployment failed: $message" fi
_debug "Response" "$response" ;;
return 1 esac
fi _err "Certificate deployment failed (HTTP status $_status_code). $message"
_debug "Response" "$response"
return 1
} }

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

@@ -344,6 +344,7 @@ synology_dsm_deploy() {
else else
_err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more." _err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more."
fi fi
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1 return 1
fi fi
@@ -354,6 +355,7 @@ synology_dsm_deploy() {
if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then 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" _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1 return 1
fi fi
@@ -389,13 +391,13 @@ synology_dsm_deploy() {
else else
_info "Restart HTTP services not necessary." _info "Restart HTTP services not necessary."
fi fi
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_logout _logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 0 return 0
else else
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_err "Unable to update certificate, got error response: $response." _err "Unable to update certificate, got error response: $response."
_logout _logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1 return 1
fi fi
} }
@@ -403,6 +405,8 @@ synology_dsm_deploy() {
#################### Private functions below ################################## #################### Private functions below ##################################
_logout() { _logout() {
# Logout CERT user only to not occupy a permanent session, e.g. in DSM's "Connected Users" widget (based on previous variables) # 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") response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid")
_debug3 response "$response" _debug3 response "$response"
} }

315
deploy/unifios.sh Normal file
View File

@@ -0,0 +1,315 @@
#!/usr/bin/env sh
# Deploy hook for UniFi OS, via the certificate REST API.
#
# Works against any UniFi OS whose management UI exposes
# /api/userCertificates. Confirmed on:
# - UniFi OS Server (the separately-installed, self-hosted application)
# on macOS and on Linux. Windows should also work (it runs under
# WSL2), but has not been tested.
# Tested on: Ubuntu 26.04 (remote) and macOS 26.6 (local).
# - UniFi OS hardware: UDM Pro on UniFi OS 5.1.26, UCG Fiber on
# UniFi OS 5.0.16 (user reports, see issues 7184 and 6916).
# No lower version bound is claimed -- if the UI has a certificate
# manager, this hook should work.
#
# `unifios` vs `unifi`: the split is the access method, not the product
# line. `unifi` writes files / a Java keystore and needs local or SSH
# access on the device; this hook drives the same REST API the web UI
# uses and works remotely. Use `unifi` where acme.sh runs on the device
# itself, this hook where it does not.
#
# The API is served on the management port, which differs per install:
# UniFi OS Server listens on 11443 (hence the default below), while
# UniFi OS hardware serves it on 443 -- set DEPLOY_UNIFIOS_HOST to
# "https://<host>" there.
#
# Endpoints used, all as the web UI itself calls them:
# 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 may be
# self-signed -- it always is on a fresh install, and there is no reliable
# way to tell in advance whether an earlier run has already replaced it --
# 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", i.e. a UniFi OS Server on the
# same machine as acme.sh; set it to "https://<host>" for UniFi OS
# hardware or any remote target)
# DEPLOY_UNIFIOS_USERNAME - UniFi OS admin username (required)
# DEPLOY_UNIFIOS_PASSWORD - UniFi OS 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
}

View File

@@ -9,7 +9,7 @@ Options:
AZUREDNS_APPID App ID. App ID of the service principal AZUREDNS_APPID App ID. App ID of the service principal
AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal
AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false" AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false"
AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Optional. AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Not saved, provide it on every run. Optional.
' '
wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS
@@ -47,13 +47,15 @@ dns_azure_add() {
_saveaccountconf_mutable AZUREDNS_TENANTID "" _saveaccountconf_mutable AZUREDNS_TENANTID ""
_saveaccountconf_mutable AZUREDNS_APPID "" _saveaccountconf_mutable AZUREDNS_APPID ""
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET "" _saveaccountconf_mutable AZUREDNS_CLIENTSECRET ""
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN "" _clearaccountconf_mutable AZUREDNS_BEARERTOKEN
else else
_info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token" _info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token"
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}" #AZUREDNS_BEARERTOKEN is short-lived, so it is taken from the environment only and never
#read from or saved to the account conf. Versions up to 3.0.9 cached their internal access
#token under the same name, which must not be replayed as a user token (#7218).
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
if [ -z "$AZUREDNS_TENANTID" ]; then if [ -z "$AZUREDNS_TENANTID" ]; then
AZUREDNS_SUBSCRIPTIONID="" AZUREDNS_SUBSCRIPTIONID=""
@@ -93,7 +95,7 @@ dns_azure_add() {
_saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID" _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
_saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID" _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET" _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$AZUREDNS_BEARERTOKEN" _clearaccountconf_mutable AZUREDNS_BEARERTOKEN
fi fi
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
@@ -175,7 +177,7 @@ dns_azure_rm() {
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}" #AZUREDNS_BEARERTOKEN comes from the environment only, see the note in dns_azure_add
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
if [ -z "$AZUREDNS_TENANTID" ]; then if [ -z "$AZUREDNS_TENANTID" ]; then
AZUREDNS_SUBSCRIPTIONID="" AZUREDNS_SUBSCRIPTIONID=""

View File

@@ -323,21 +323,21 @@ _bhosted_extract_id() {
fi fi
# JSON: "id":12345 # JSON: "id":12345
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[[:space:]]*:[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')" _id="$(printf "%s" "$_resp" | _egrep_o '"id"[ ]*:[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
if [ -n "$_id" ]; then if [ -n "$_id" ]; then
printf "%s" "$_id" printf "%s" "$_id"
return 0 return 0
fi fi
# key=value: id=12345 # key=value: id=12345
_id="$(printf "%s" "$_resp" | _egrep_o '(^|[[:space:][:punct:]])id[[:space:]]*=[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')" _id="$(printf "%s" "$_resp" | _egrep_o '(^|[^0-9a-zA-Z])id[ ]*=[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
if [ -n "$_id" ]; then if [ -n "$_id" ]; then
printf "%s" "$_id" printf "%s" "$_id"
return 0 return 0
fi fi
# "record id 12345" / "recordid 12345" # "record id 12345" / "recordid 12345"
_id="$(printf "%s" "$_resp" | _egrep_o '(record[[:space:]]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')" _id="$(printf "%s" "$_resp" | _egrep_o '(record[ ]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
if [ -n "$_id" ]; then if [ -n "$_id" ]; then
printf "%s" "$_id" printf "%s" "$_id"
return 0 return 0

View File

@@ -75,7 +75,7 @@ dns_creoline_rm() {
return 1 return 1
fi fi
record_id=$(echo "$response" | _egrep_o "\"id\"[[:space:]]*:[[:space:]]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") 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" _debug "record_id" "$record_id"
if [ -z "$record_id" ]; then if [ -z "$record_id" ]; then
@@ -108,10 +108,10 @@ _get_root() {
return 1 return 1
fi fi
_sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") _sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
_debug _sub_domain "$_sub_domain" _debug _sub_domain "$_sub_domain"
_domain=$(echo "$response" | _egrep_o "\"domain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") _domain=$(echo "$response" | _egrep_o "\"domain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
_debug _domain "$_domain" _debug _domain "$_domain"
if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then
@@ -171,7 +171,7 @@ _creoline_rest() {
_err "URI:$uri" _err "URI:$uri"
return 1 return 1
elif _contains "$response" "message"; then elif _contains "$response" "message"; then
message=$(echo "$response" | _egrep_o "\"message\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \") message=$(echo "$response" | _egrep_o "\"message\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \")
_err "Error: $message" _err "Error: $message"
_err "URI:$uri" _err "URI:$uri"
return 1 return 1

View File

@@ -285,15 +285,15 @@ _cyon_delete_txt() {
list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async" 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}" _debug list_txt_response "${list_txt_response}"
if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi
# Find and delete all acme challenge entries for the $fulldomain. # 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_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)"
dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)" dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)"

View File

@@ -30,8 +30,9 @@ dns_czechia_add() {
return 1 return 1
fi fi
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') _czechia_tab="$(printf '\t')"
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') _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 if [ -z "$_cz" ] || [ -z "$_tk" ]; then
_err "Missing zone or CZ_AuthorizationToken." _err "Missing zone or CZ_AuthorizationToken."
@@ -108,8 +109,9 @@ dns_czechia_rm() {
return 1 return 1
fi fi
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') _czechia_tab="$(printf '\t')"
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') _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 if [ -z "$_cz" ] || [ -z "$_tk" ]; then
_err "Missing zone or CZ_AuthorizationToken." _err "Missing zone or CZ_AuthorizationToken."
@@ -180,12 +182,13 @@ _czechia_load_conf() {
} }
_czechia_pick_zone() { _czechia_pick_zone() {
_czechia_pz_tab="$(printf '\t')"
_fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//') _fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//')
_best_zone="" _best_zone=""
_zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g') _zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g')
for _z in $_zones_space; do for _z in $_zones_space; do
_clean_z=$(printf "%s" "$_z" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') _clean_z=$(printf "%s" "$_z" | _lower_case | sed "s/[ $_czechia_pz_tab]//g; s/\.\$//")
[ -z "$_clean_z" ] && continue [ -z "$_clean_z" ] && continue
case "$_fd" in case "$_fd" in

View File

@@ -75,6 +75,11 @@ dns_easydns_rm() {
EASYDNS_Token="${EASYDNS_Token:-$(_readaccountconf_mutable EASYDNS_Token)}" EASYDNS_Token="${EASYDNS_Token:-$(_readaccountconf_mutable EASYDNS_Token)}"
EASYDNS_Key="${EASYDNS_Key:-$(_readaccountconf_mutable EASYDNS_Key)}" EASYDNS_Key="${EASYDNS_Key:-$(_readaccountconf_mutable EASYDNS_Key)}"
if [ -z "$EASYDNS_Token" ] || [ -z "$EASYDNS_Key" ]; then
_err "You didn't specify an easydns.net token or api key. Signup at https://cp.easydns.com/manage/security/api/signup.php"
return 1
fi
_debug "First detect the root zone" _debug "First detect the root zone"
if ! _get_root "$fulldomain"; then if ! _get_root "$fulldomain"; then
_err "invalid domain" _err "invalid domain"
@@ -91,24 +96,21 @@ dns_easydns_rm() {
return 1 return 1
fi fi
count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) record_id=$(printf "%s\n" "$response" | tr '{' '\n' | grep "\"rdata\":\"$txtvalue\"" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
_debug count "$count" _debug "record_id" "$record_id"
if [ "$count" = "0" ]; then
if [ -z "$record_id" ]; then
_info "Don't need to remove." _info "Don't need to remove."
else return 0
record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1)
_debug "record_id" "$record_id"
if [ -z "$record_id" ]; then
_err "Can not get record id to remove."
return 1
fi
if ! _EASYDNS_rest DELETE "zones/records/$_domain/$record_id"; then
_err "Delete record error."
return 1
fi
_contains "$response" "\"status\":200"
fi fi
if ! _EASYDNS_rest DELETE "zones/records/$_domain/$record_id"; then
_err "Delete record error."
return 1
fi
_contains "$response" "\"status\":200"
} }
#################### Private functions below ################################## #################### Private functions below ##################################

View File

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

198
dnsapi/dns_hestiacp.sh Normal file
View File

@@ -0,0 +1,198 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_hestiacp_info='HestiaCP Server API
Site: hestiacp.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hestiacp
Options:
HESTIA_HOST Panel URL. E.g. "https://panel.example.com:8083"
HESTIA_ACCESS API access key
HESTIA_SECRET API secret key
HESTIA_USER Username owning the DNS zones. Default "admin". Optional.
Issues: github.com/acmesh-official/acme.sh/issues/6251
Author: Radu Malica <radu.malica@gmail.com>
'
######## Public functions #####################
# Usage: dns_hestiacp_add fulldomain txtvalue
dns_hestiacp_add() {
fulldomain=$1
txtvalue=$2
if ! _hestia_init; then
return 1
fi
_debug "Detecting the root zone for $fulldomain"
if ! _hestia_get_root "$fulldomain"; then
_err "Cannot find a DNS zone for $fulldomain under user $HESTIA_USER"
return 1
fi
_debug _hestia_domain "$_hestia_domain"
_debug _hestia_sub "$_hestia_sub"
# _hestia_get_root left the zone record listing in _hestia_response
if _hestia_find_records "$_hestia_sub" "TXT" | grep -F -- "$txtvalue" >/dev/null; then
_info "The TXT record already exists, skipping"
return 0
fi
_info "Adding TXT record for $fulldomain"
if ! _hestia_rest "v-add-dns-record" "$HESTIA_USER" "$_hestia_domain" "$_hestia_sub" "TXT" "$txtvalue" "" "" "yes" "600"; then
_err "Error adding TXT record: $_hestia_response"
return 1
fi
_info "TXT record added successfully"
return 0
}
# Usage: dns_hestiacp_rm fulldomain txtvalue
dns_hestiacp_rm() {
fulldomain=$1
txtvalue=$2
if ! _hestia_init; then
return 1
fi
_debug "Detecting the root zone for $fulldomain"
if ! _hestia_get_root "$fulldomain"; then
_err "Cannot find a DNS zone for $fulldomain under user $HESTIA_USER"
return 1
fi
_debug _hestia_domain "$_hestia_domain"
_debug _hestia_sub "$_hestia_sub"
_hestia_removed=0
_hestia_failed=0
while IFS='|' read -r _hestia_id _hestia_value || [ -n "$_hestia_id" ]; do
if [ -z "$_hestia_id" ]; then
continue
fi
if ! _contains "$_hestia_value" "$txtvalue"; then
continue
fi
_info "Deleting TXT record $_hestia_id"
if ! _hestia_rest "v-delete-dns-record" "$HESTIA_USER" "$_hestia_domain" "$_hestia_id" "yes"; then
_err "Error deleting TXT record $_hestia_id: $_hestia_response"
_hestia_failed=$(_math "$_hestia_failed" + 1)
continue
fi
_hestia_removed=$(_math "$_hestia_removed" + 1)
done <<EOF
$(_hestia_find_records "$_hestia_sub" "TXT")
EOF
if [ "$_hestia_removed" = "0" ] && [ "$_hestia_failed" = "0" ]; then
_info "No matching TXT record found to remove"
else
_info "Removed $_hestia_removed TXT record(s)"
fi
if [ "$_hestia_failed" != "0" ]; then
return 1
fi
return 0
}
#################### Private functions below ##################################
_hestia_init() {
HESTIA_HOST="${HESTIA_HOST:-$(_readaccountconf_mutable HESTIA_HOST)}"
HESTIA_ACCESS="${HESTIA_ACCESS:-$(_readaccountconf_mutable HESTIA_ACCESS)}"
HESTIA_SECRET="${HESTIA_SECRET:-$(_readaccountconf_mutable HESTIA_SECRET)}"
HESTIA_USER="${HESTIA_USER:-$(_readaccountconf_mutable HESTIA_USER)}"
if [ -z "$HESTIA_HOST" ] || [ -z "$HESTIA_ACCESS" ] || [ -z "$HESTIA_SECRET" ]; then
HESTIA_HOST=""
HESTIA_ACCESS=""
HESTIA_SECRET=""
_err "You must export HESTIA_HOST, HESTIA_ACCESS and HESTIA_SECRET first"
return 1
fi
HESTIA_HOST="${HESTIA_HOST%/}"
if ! echo "$HESTIA_HOST" | grep -qE '^https?://[^/]+$'; then
_err "HESTIA_HOST must be a valid URL (e.g. https://panel.example.com:8083)"
return 1
fi
if [ -z "$HESTIA_USER" ]; then
HESTIA_USER="admin"
fi
_saveaccountconf_mutable HESTIA_HOST "$HESTIA_HOST"
_saveaccountconf_mutable HESTIA_ACCESS "$HESTIA_ACCESS"
_saveaccountconf_mutable HESTIA_SECRET "$HESTIA_SECRET"
_saveaccountconf_mutable HESTIA_USER "$HESTIA_USER"
return 0
}
# Walk up the domain labels until the API returns a DNS zone.
# Sets _hestia_domain to the zone and _hestia_sub to the record name
# relative to the zone. The zone record listing stays in _hestia_response.
_hestia_get_root() {
_hestia_fqdn="${1%.}"
_hestia_i=1
while true; do
_hestia_h=$(printf "%s" "$_hestia_fqdn" | cut -d . -f "$_hestia_i"-100)
_debug2 _hestia_h "$_hestia_h"
if [ -z "$_hestia_h" ]; then
return 1
fi
if _hestia_rest "v-list-dns-records" "$HESTIA_USER" "$_hestia_h" "json"; then
_hestia_domain="$_hestia_h"
if [ "$_hestia_h" = "$_hestia_fqdn" ]; then
_hestia_sub="@"
else
_hestia_sub=$(printf "%s" "$_hestia_fqdn" | cut -d . -f 1-"$(_math "$_hestia_i" - 1)")
fi
return 0
fi
_hestia_i=$(_math "$_hestia_i" + 1)
done
}
# Call the HestiaCP API. Args: cmd [arg1 arg2 ...]
# The response body is stored in _hestia_response.
_hestia_rest() {
_hestia_cmd=$1
shift
_hestia_data="{\"access_key\":\"$HESTIA_ACCESS\",\"secret_key\":\"$HESTIA_SECRET\",\"cmd\":\"$_hestia_cmd\""
_hestia_argn=1
for _hestia_arg in "$@"; do
_hestia_data="$_hestia_data,\"arg$_hestia_argn\":\"$_hestia_arg\""
_hestia_argn=$(_math "$_hestia_argn" + 1)
done
_hestia_data="$_hestia_data}"
_debug2 "Calling $_hestia_cmd"
_hestia_response=$(_post "$_hestia_data" "$HESTIA_HOST/api/" "" "POST" "application/json")
_hestia_ret=$?
_debug2 _hestia_response "$_hestia_response"
if [ "$_hestia_ret" != "0" ]; then
_err "Error connecting to the HestiaCP API"
return 1
fi
if _contains "$_hestia_response" "Error:"; then
return 1
fi
return 0
}
# Extract records matching name and type from the v-list-dns-records
# response in _hestia_response. Prints one "id|value" line per match.
_hestia_find_records() {
_hestia_fname=$1
_hestia_ftype=$2
echo "$_hestia_response" | tr -d '\n' | sed 's/},/}\
/g' | grep -F -- "\"RECORD\": \"$_hestia_fname\"" | grep -F -- "\"TYPE\": \"$_hestia_ftype\"" | while read -r _hestia_line; do
_hestia_id=$(echo "$_hestia_line" | _egrep_o '"ID": "[^"]*' | cut -d '"' -f 4)
_hestia_value=$(echo "$_hestia_line" | _egrep_o '"VALUE": "[^"]*' | cut -d '"' -f 4)
if [ -n "$_hestia_id" ]; then
echo "$_hestia_id|$_hestia_value"
fi
done
}

View File

@@ -441,18 +441,18 @@ _hostup_json_extract() {
input="${2:-$line}" input="${2:-$line}"
# First try to extract quoted values (strings) # First try to extract quoted values (strings)
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | _head_n 1)" quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[ ]*:[ ]*\"[^\"]*\"" | _head_n 1)"
if [ -n "$quoted_match" ]; then if [ -n "$quoted_match" ]; then
printf "%s" "$quoted_match" | printf "%s" "$quoted_match" |
cut -d : -f2- | cut -d : -f2- |
sed 's/^[[:space:]]*"//' | sed 's/^[ ]*"//' |
sed 's/"[[:space:]]*$//' | sed 's/"[ ]*$//' |
sed 's/\\"/"/g' sed 's/\\"/"/g'
return 0 return 0
fi fi
# Fallback for unquoted values (e.g., numeric IDs) # Fallback for unquoted values (e.g., numeric IDs)
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*[^,}]*" | _head_n 1)" unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[ ]*:[ ]*[^,}]*" | _head_n 1)"
if [ -n "$unquoted_match" ]; then if [ -n "$unquoted_match" ]; then
printf "%s" "$unquoted_match" | printf "%s" "$unquoted_match" |
cut -d : -f2- | cut -d : -f2- |

View File

@@ -117,7 +117,7 @@ dns_infoblox_uddi_rm() {
return 0 return 0
fi fi
record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4) record_id=$(echo "$response" | _egrep_o '"id":[ ]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_debug "record_id" "$record_id" _debug "record_id" "$record_id"
if [ -z "$record_id" ]; then if [ -z "$record_id" ]; then
@@ -178,7 +178,7 @@ _get_root() {
# Check if response contains results (even if empty) # Check if response contains results (even if empty)
if _contains "$response" '"results"'; then if _contains "$response" '"results"'; then
# Extract zone ID - must match the pattern dns/auth_zone/... # Extract zone ID - must match the pattern dns/auth_zone/...
zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4) zone_id=$(echo "$response" | _egrep_o '"id":[ ]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
if [ -n "$zone_id" ]; then if [ -n "$zone_id" ]; then
# Found the zone # Found the zone
_domain="$h" _domain="$h"

View File

@@ -7,22 +7,23 @@ Options:
JD_ACCESS_KEY_ID Access key ID JD_ACCESS_KEY_ID Access key ID
JD_ACCESS_KEY_SECRET Access key secret JD_ACCESS_KEY_SECRET Access key secret
JD_REGION Region. E.g. "cn-north-1" JD_REGION Region. E.g. "cn-north-1"
Issues: github.com/acmesh-official/acme.sh/issues/2388 Issues: github.com/acmesh-official/acme.sh/issues/7202
Author: @skysaint
' '
_JD_ACCOUNT="https://uc.jdcloud.com/account/accesskey" _JD_ACCOUNT="https://uc.jdcloud.com/account/accesskey"
_JD_PROD="clouddnsservice" _JD_PROD="domainservice"
_JD_API="jdcloud-api.com" _JD_API="jdcloud-api.com"
_JD_API_VERSION="v1" _JD_API_VERSION="v2"
_JD_DEFAULT_REGION="cn-north-1" _JD_DEFAULT_REGION="cn-north-1"
_JD_HOST="$_JD_PROD.$_JD_API" _JD_HOST="$_JD_PROD.$_JD_API"
######## Public functions ##################### ######## Public functions #####################
#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" #Usage: dns_jd_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_jd_add() { dns_jd_add() {
fulldomain=$1 fulldomain=$1
txtvalue=$2 txtvalue=$2
@@ -58,24 +59,14 @@ dns_jd_add() {
_debug _sub_domain "$_sub_domain" _debug _sub_domain "$_sub_domain"
_debug _domain "$_domain" _debug _domain "$_domain"
#_debug "Getting getViewTree" #_debug "Getting describeViewTree"
_debug "Adding records" _debug "Adding records"
_addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1},\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}" _addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1}}"
#_addrr='{"req":{"hostRecord":"xx","hostValue":"\"value4\"","jcloudRes":false,"mxPriority":null,"port":null,"ttl":300,"type":"TXT","weight":null,"viewValue":-1},"regionId":"cn-north-1","domainId":"8824"}' #_addrr='{"req":{"hostRecord":"_acme-challenge","hostValue":"XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs","ttl":300,"type":"TXT","viewValue":-1}}'
if jd_rest POST "domain/$_domain_id/RRAdd" "" "$_addrr"; then if jd_rest POST "domain/$_domain_id/ResourceRecord" "" "$_addrr"; then
_rid="$(echo "$response" | tr '{},' '\n' | grep '"id":' | cut -d : -f 2)"
if [ -z "$_rid" ]; then
_err "Can not find record id from the result."
return 1
fi
_info "TXT record added successfully." _info "TXT record added successfully."
_srid="$(_readdomainconf "JD_CLOUD_RIDS")"
if [ "$_srid" ]; then
_rid="$_srid,$_rid"
fi
_savedomainconf "JD_CLOUD_RIDS" "$_rid"
return 0 return 0
fi fi
@@ -97,14 +88,7 @@ dns_jd_rm() {
_JD_BASE_URI="$_JD_API_VERSION/regions/$JD_REGION" _JD_BASE_URI="$_JD_API_VERSION/regions/$JD_REGION"
_info "Getting existing records for $fulldomain" _info "Removing TXT record for $fulldomain"
_srid="$(_readdomainconf "JD_CLOUD_RIDS")"
_debug _srid "$_srid"
if [ -z "$_srid" ]; then
_err "Not rid skip"
return 0
fi
_debug "First detect the root zone" _debug "First detect the root zone"
if ! _get_root "$fulldomain"; then if ! _get_root "$fulldomain"; then
@@ -115,16 +99,37 @@ dns_jd_rm() {
_debug _sub_domain "$_sub_domain" _debug _sub_domain "$_sub_domain"
_debug _domain "$_domain" _debug _domain "$_domain"
_cleardomainconf JD_CLOUD_RIDS # List records, filter by hostRecord and use a large pageSize so it isn't missed on record-heavy zones.
if ! jd_rest GET "domain/$_domain_id/ResourceRecord" "pageSize=50&search=$_sub_domain"; then
_err "Failed to list resource records"
return 1
fi
_aws_tmpl_xml="{\"ids\":[$_srid],\"action\":\"del\",\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}" # Match record by hostRecord + type TXT + hostValue
_record_id=""
_matched="$(echo "$response" | tr '{' '\n' | grep "\"hostRecord\":\"$_sub_domain\"" | grep "\"type\":\"TXT\"" | grep "\"hostValue\":\"$txtvalue\"")"
_debug2 _matched "$_matched"
if jd_rest POST "domain/$_domain_id/RROperate" "" "$_aws_tmpl_xml" && _contains "$response" "\"code\":\"OK\""; then if [ -z "$_matched" ]; then
_info "TXT record not found, nothing to remove."
return 0
fi
_record_id="$(echo "$_matched" | tr ',' '\n' | grep "\"id\":" | cut -d : -f 2 | tr -d '"' | _head_n 1)"
_debug _record_id "$_record_id"
if [ -z "$_record_id" ]; then
_info "Could not extract record id from response, nothing to remove."
return 0
fi
if jd_rest DELETE "domain/$_domain_id/ResourceRecord/$_record_id"; then
_info "TXT record deleted successfully." _info "TXT record deleted successfully."
return 0 return 0
fi fi
return 1
_err "Failed to delete TXT record."
return 1
} }
#################### Private functions below ################################## #################### Private functions below ##################################
@@ -134,13 +139,14 @@ _get_root() {
i=1 i=1
p=1 p=1
if ! jd_rest GET "domain"; then
_err "error get domain list"
return 1
fi
while true; do while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug2 "Checking domain: $h" _debug2 "Checking domain: $h"
if ! jd_rest GET "domain"; then
_err "error get domain list"
return 1
fi
if [ -z "$h" ]; then if [ -z "$h" ]; then
#not valid #not valid
_err "Invalid domain" _err "Invalid domain"
@@ -168,6 +174,8 @@ _get_root() {
return 1 return 1
} }
# Use '%b' with printf to expand \n escapes in CanonicalRequest and StringToSign.
# Use '%s' for plain values that contain no escapes to avoid unintended expansion.
#method uri qstr data #method uri qstr data
jd_rest() { jd_rest() {
mtd="$1" mtd="$1"
@@ -220,7 +228,7 @@ jd_rest() {
CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$RequestPayloadHash" CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$RequestPayloadHash"
_debug2 CanonicalRequest "$CanonicalRequest" _debug2 CanonicalRequest "$CanonicalRequest"
HashedCanonicalRequest="$(printf "$CanonicalRequest%s" | _digest "$Hash" hex)" HashedCanonicalRequest="$(printf '%b' "$CanonicalRequest" | _digest "$Hash" hex)"
_debug2 HashedCanonicalRequest "$HashedCanonicalRequest" _debug2 HashedCanonicalRequest "$HashedCanonicalRequest"
Algorithm="JDCLOUD2-HMAC-SHA256" Algorithm="JDCLOUD2-HMAC-SHA256"
@@ -246,19 +254,19 @@ jd_rest() {
kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")" kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")"
_secure_debug2 kSecretH "$kSecretH" _secure_debug2 kSecretH "$kSecretH"
kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)" kDateH="$(printf '%s' "$RequestDateOnly" | _hmac "$Hash" "$kSecretH" hex)"
_debug2 kDateH "$kDateH" _debug2 kDateH "$kDateH"
kRegionH="$(printf "$Region%s" | _hmac "$Hash" "$kDateH" hex)" kRegionH="$(printf '%s' "$Region" | _hmac "$Hash" "$kDateH" hex)"
_debug2 kRegionH "$kRegionH" _debug2 kRegionH "$kRegionH"
kServiceH="$(printf "$Service%s" | _hmac "$Hash" "$kRegionH" hex)" kServiceH="$(printf '%s' "$Service" | _hmac "$Hash" "$kRegionH" hex)"
_debug2 kServiceH "$kServiceH" _debug2 kServiceH "$kServiceH"
kSigningH="$(printf "%s" "jdcloud2_request" | _hmac "$Hash" "$kServiceH" hex)" kSigningH="$(printf '%s' "jdcloud2_request" | _hmac "$Hash" "$kServiceH" hex)"
_debug2 kSigningH "$kSigningH" _debug2 kSigningH "$kSigningH"
signature="$(printf "$StringToSign%s" | _hmac "$Hash" "$kSigningH" hex)" signature="$(printf '%b' "$StringToSign" | _hmac "$Hash" "$kSigningH" hex)"
_debug2 signature "$signature" _debug2 signature "$signature"
Authorization="$Algorithm Credential=$JD_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature" Authorization="$Algorithm Credential=$JD_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"

View File

@@ -1,189 +0,0 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_linode_info='Linode.com (Old)
Deprecated. Use dns_linode_v4
Site: Linode.com
Options:
LINODE_API_KEY API Key
Author: Philipp Grosswiler <philipp.grosswiler@swiss-design.net>
'
LINODE_API_URL="https://api.linode.com/?api_key=$LINODE_API_KEY&api_action="
######## Public functions #####################
#Usage: dns_linode_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_linode_add() {
fulldomain="${1}"
txtvalue="${2}"
if ! _Linode_API; then
return 1
fi
_info "Using Linode"
_debug "Calling: dns_linode_add() '${fulldomain}' '${txtvalue}'"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Domain does not exist."
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_parameters="&DomainID=$_domain_id&Type=TXT&Name=$_sub_domain&Target=$txtvalue"
if _rest GET "domain.resource.create" "$_parameters" && [ -n "$response" ]; then
_resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
_debug _resource_id "$_resource_id"
if [ -z "$_resource_id" ]; then
_err "Error adding the domain resource."
return 1
fi
_info "Domain resource successfully added."
return 0
fi
return 1
}
#Usage: dns_linode_rm _acme-challenge.www.domain.com
dns_linode_rm() {
fulldomain="${1}"
if ! _Linode_API; then
return 1
fi
_info "Using Linode"
_debug "Calling: dns_linode_rm() '${fulldomain}'"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Domain does not exist."
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_parameters="&DomainID=$_domain_id"
if _rest GET "domain.resource.list" "$_parameters" && [ -n "$response" ]; then
response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")"
resource="$(echo "$response" | _egrep_o "{.*\"NAME\":\s*\"$_sub_domain\".*}")"
if [ "$resource" ]; then
_resource_id=$(printf "%s\n" "$resource" | _egrep_o "\"RESOURCEID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
if [ "$_resource_id" ]; then
_debug _resource_id "$_resource_id"
_parameters="&DomainID=$_domain_id&ResourceID=$_resource_id"
if _rest GET "domain.resource.delete" "$_parameters" && [ -n "$response" ]; then
_resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
_debug _resource_id "$_resource_id"
if [ -z "$_resource_id" ]; then
_err "Error deleting the domain resource."
return 1
fi
_info "Domain resource successfully deleted."
return 0
fi
fi
return 1
fi
return 0
fi
return 1
}
#################### Private functions below ##################################
_Linode_API() {
if [ -z "$LINODE_API_KEY" ]; then
LINODE_API_KEY=""
_err "You didn't specify the Linode API key yet."
_err "Please create your key and try again."
return 1
fi
_saveaccountconf LINODE_API_KEY "$LINODE_API_KEY"
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_id=12345
_get_root() {
domain=$1
i=2
p=1
if _rest GET "domain.list"; then
response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")"
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
#not valid
return 1
fi
hostedzone="$(echo "$response" | _egrep_o "{.*\"DOMAIN\":\s*\"$h\".*}")"
if [ "$hostedzone" ]; then
_domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"DOMAINID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
fi
return 1
fi
p=$i
i=$(_math "$i" + 1)
done
fi
return 1
}
#method method action data
_rest() {
mtd="$1"
ep="$2"
data="$3"
_debug mtd "$mtd"
_debug ep "$ep"
export _H1="Accept: application/json"
export _H2="Content-Type: application/json"
if [ "$mtd" != "GET" ]; then
# both POST and DELETE.
_debug data "$data"
response="$(_post "$data" "$LINODE_API_URL$ep" "" "$mtd")"
else
response="$(_get "$LINODE_API_URL$ep$data")"
fi
if [ "$?" != "0" ]; then
_err "error $ep"
return 1
fi
_debug2 response "$response"
return 0
}

121
dnsapi/dns_myloc.sh Executable file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_myloc_info='myloc.de
Site: myloc.de
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_myloc
Issues: github.com/acmesh-official/acme.sh/issues/5193
Options:
MYLOC_token API token
'
# updater for the (experimental) API of myloc.de / webtropia.com
# usage: acme.sh --issue -d example.com --dns dns_myloc --dnssleep 60
# API documentation at https://apidoc.myloc.de/
# As the API does not support quering available zones yet, the zone for a given
# fulldomain is searched recursively by removing prefixes one-by-one.
_myloc_api="https://zkm.myloc.de/api"
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_myloc_add() {
_myloc_fulldomain=$1
_myloc_txtvalue=$2
_myloc_token="${MYLOC_token:-$(_readaccountconf_mutable MYLOC_token)}"
if [ -z "$_myloc_token" ]; then
_err "You didn't specify MYLOC_token"
return 1
fi
export _H1="Content-Type: application/json"
export _H2="Authorization: Bearer $_myloc_token"
_myloc_zone="$(_myloc_get_zone "$_myloc_fulldomain")"
if [ $? -ne 0 ]; then
return 1
fi
# save token if the previous request was successful
_saveaccountconf_mutable MYLOC_token "$_myloc_token"
_info "Adding record"
_myloc_record="{\"type\":\"TXT\",\"name\":\"${_myloc_fulldomain}\",\"content\":\"\\\"${_myloc_txtvalue}\\\"\",\"ttl\":60}"
_debug "add record request $_myloc_record to ${_myloc_api}/dns/zone/${_myloc_zone}"
_myloc_response="$(_post "$_myloc_record" "${_myloc_api}/dns/zone/${_myloc_zone}" "" "PUT")"
_myloc_status=$?
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug "add record response $_code $_myloc_response"
if [ $_myloc_status -ne 0 ]; then
_err "Add txt record curl error."
return 1
elif [ "$_code" = "204" ] && [ -z "$_myloc_response" ]; then
_info "Add txt record success"
return 0
elif _contains "$_myloc_response" "error" || _contains "$_myloc_response" "unexpected"; then
_err "Add txt record api error."
return 1
else
_err "Add txt record unknown response."
return 1
fi
}
#_myloc_fulldomain _myloc_txtvalue
dns_myloc_rm() {
_myloc_fulldomain=$1
_myloc_txtvalue=$2
_myloc_token="${MYLOC_token:-$(_readaccountconf_mutable MYLOC_token)}"
if [ -z "$_myloc_token" ]; then
_err "You didn't specify MYLOC_token"
return 1
fi
export _H1="Content-Type: application/json"
export _H2="Authorization: Bearer $_myloc_token"
_myloc_zone="$(_myloc_get_zone "$_myloc_fulldomain")"
if [ $? -ne 0 ]; then
return 1
fi
# save token if the previous request was successful
_saveaccountconf_mutable MYLOC_token "$_myloc_token"
_info "Deleting record for $_myloc_fulldomain"
_myloc_record="{\"type\":\"TXT\",\"name\":\"${_myloc_fulldomain}\",\"content\":\"\\\"${_myloc_txtvalue}\\\"\"}"
_debug "delete record $_myloc_record"
_myloc_response="$(_post "$_myloc_record" "${_myloc_api}/dns/zone/${_myloc_zone}" "" "DELETE")"
_myloc_status=$?
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug "delete response $_code $_myloc_response"
if [ $_myloc_status -ne 0 ] || [ "$_code" != "204" ]; then
_err "Failed to delete record"
return 1
fi
return 0
}
# Usage: _myloc_get_zone "_acme-challenge.sub1.mydomain.com"
# Subdomains are walked until a zone is found or TLD is reached
_myloc_get_zone() {
_myloc_zone=$1
while [ "${_myloc_zone#*.}" != "$_myloc_zone" ]; do
_debug "Get zone trying $_myloc_zone"
_myloc_response="$(_get "${_myloc_api}/dns/zone/${_myloc_zone}")"
_myloc_status=$?
_debug "Get zone response $_myloc_response"
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
if [ $_myloc_status -eq 0 ] && [ "$_code" = "200" ]; then
_debug "Get zone success for $_myloc_zone"
echo "${_myloc_zone}"
return 0
fi
_myloc_zone="${_myloc_zone#*.}"
done
_err "Get zone failed for all candidates"
return 1
}

View File

@@ -104,6 +104,9 @@ _get_root_by_getList() {
return 1 return 1
fi fi
_namecheap_domain_list=$(echo "$response" | _egrep_o '<Domain [^>]*')
_debug2 domain_list "$_namecheap_domain_list"
i=2 i=2
p=1 p=1
@@ -120,7 +123,7 @@ _get_root_by_getList() {
return 1 return 1
fi fi
if ! _contains "$response" "$h"; then if ! _namecheap_is_our_dns "$h"; then
_debug "$h not found" _debug "$h not found"
else else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
@@ -133,6 +136,29 @@ _get_root_by_getList() {
return 1 return 1
} }
#Usage: _namecheap_is_our_dns <domain>
#Succeeds only when domains.getList listed exactly <domain> AND that entry is
#served by Namecheap's own DNS. A domain parked on Namecheap's webhosting DNS
#is listed with IsOurDNS="false", and every dns.getHosts/setHosts call against
#it is refused with error 2030288 "not using proper DNS servers". Accepting
#such a domain as the root zone hides a subdomain that IS delegated to
#Namecheap DNS and that the getHosts probe below would have found.
#https://github.com/acmesh-official/acme.sh/issues/7178
_namecheap_is_our_dns() {
_namecheap_entry=$(echo "$_namecheap_domain_list" | grep -F " Name=\"$1\"" | _head_n 1)
if [ -z "$_namecheap_entry" ]; then
return 1
fi
_namecheap_ourdns=$(echo "$_namecheap_entry" | _egrep_o ' IsOurDNS="[^"]*' | cut -d '"' -f 2)
_debug2 "$1 IsOurDNS" "$_namecheap_ourdns"
if [ "$_namecheap_ourdns" = "true" ]; then
return 0
fi
return 1
}
_get_root_by_getHosts() { _get_root_by_getHosts() {
i=100 i=100
p=99 p=99

View File

@@ -33,9 +33,11 @@ dns_netcup_add() {
exit=$(echo "$fulldomain" | tr -dc '.' | wc -c) exit=$(echo "$fulldomain" | tr -dc '.' | wc -c)
exit=$(_math "$exit" + 1) exit=$(_math "$exit" + 1)
i=$exit i=$exit
_nc_last=$(_nc_lastlevel "$i")
_nc_found=""
while while
[ "$exit" -gt 0 ] [ "$exit" -ge "$_nc_last" ]
do do
tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit") tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
if [ "$(_math "$i" - "$exit")" -eq 0 ]; then if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
@@ -51,12 +53,18 @@ dns_netcup_add() {
_err "$msg" _err "$msg"
return 1 return 1
else else
_nc_found=1
break break
fi fi
fi fi
fi fi
exit=$(_math "$exit" - 1) exit=$(_math "$exit" - 1)
done done
if [ -z "$_nc_found" ]; then
_err "$msg"
_nc_nozone "$fulldomain"
return 1
fi
logout logout
} }
@@ -70,9 +78,11 @@ dns_netcup_rm() {
exit=$(_math "$exit" + 1) exit=$(_math "$exit" + 1)
i=$exit i=$exit
rec="" rec=""
_nc_last=$(_nc_lastlevel "$i")
_nc_found=""
while while
[ "$exit" -gt 0 ] [ "$exit" -ge "$_nc_last" ]
do do
tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit") tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
if [ "$(_math "$i" - "$exit")" -eq 0 ]; then if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
@@ -89,12 +99,18 @@ dns_netcup_rm() {
_err "$msg" _err "$msg"
return 1 return 1
else else
_nc_found=1
break break
fi fi
fi fi
fi fi
exit=$(_math "$exit" - 1) exit=$(_math "$exit" - 1)
done done
if [ -z "$_nc_found" ]; then
_err "$msg"
_nc_nozone "$fulldomain"
return 1
fi
ida=0000 ida=0000
idv=0001 idv=0001
@@ -125,6 +141,27 @@ dns_netcup_rm() {
logout logout
} }
# The zone is looked up by walking the challenge name from the right, one
# label at a time. The leftmost label is the challenge prefix, so the full
# name itself can never be a zone: asking netcup for it only returns 4013
# "Validation Error", which would then mask the real 5028 "zone could not be
# found". Stop one label short, unless the name is too short to have a
# challenge prefix at all (manual invocation).
# levels
_nc_lastlevel() {
if [ "$1" -ge 3 ]; then
echo 2
else
echo 1
fi
}
# fulldomain
_nc_nozone() {
_err "No DNS zone for $1 was found at netcup."
_err "Check that the domain belongs to the account of the configured NC_CID and that its DNS is hosted at netcup."
}
_login() { _login() {
tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST") tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4) sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4)

244
dnsapi/dns_nexdns.sh Executable file
View File

@@ -0,0 +1,244 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_nexdns_info='NexDNS
Site: nexdns.tech
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_nexdns
Options:
NEXDNS_Token API token. Can be created at https://nexdns.tech/settings/api-keys
NEXDNS_Api API base url. Default "https://api.nexdns.tech/v1". Optional.
Issues: github.com/acmesh-official/acme.sh/issues/7179
Author: NexDNS <https://github.com/nexdns>
'
NEXDNS_Api_Default="https://api.nexdns.tech/v1"
######## Public functions #####################
#Usage: dns_nexdns_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_nexdns_add() {
fulldomain=$1
txtvalue=$2
if ! _nexdns_init; then
return 1
fi
_saveaccountconf_mutable NEXDNS_Token "$NEXDNS_Token"
if [ "$NEXDNS_Api" != "$NEXDNS_Api_Default" ]; then
_saveaccountconf_mutable NEXDNS_Api "$NEXDNS_Api"
else
_clearaccountconf_mutable NEXDNS_Api
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Cannot find the zone of $fulldomain in this NexDNS account."
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug _domain_id "$_domain_id"
_info "Adding the TXT record for $fulldomain"
if ! _nexdns_rest POST "zones/$_domain_id/records" "{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
return 1
fi
_info "The TXT record has been added."
return 0
}
#Usage: dns_nexdns_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_nexdns_rm() {
fulldomain=$1
txtvalue=$2
if ! _nexdns_init; then
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Cannot find the zone of $fulldomain in this NexDNS account."
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug _domain_id "$_domain_id"
_info "Removing the TXT record for $fulldomain"
if ! _nexdns_rest GET "zones/$_domain_id/records?type=TXT&name=$_sub_domain"; then
return 1
fi
#All the challenge records share one name and one type, so the value is the
#only thing that tells them apart. A certificate covering example.com and
#*.example.com puts two of them at the same name at the same time.
_record_id="$(echo "$response" | tr '{' "\n" | grep -- "$txtvalue" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
_debug _record_id "$_record_id"
if [ -z "$_record_id" ]; then
_info "The TXT record is already gone, nothing to remove."
return 0
fi
if ! _nexdns_rest DELETE "zones/$_domain_id/records/$_record_id"; then
return 1
fi
_info "The TXT record has been removed."
return 0
}
#################### Private functions below ##################################
#Reads the token and the api url, and applies the default url.
_nexdns_init() {
NEXDNS_Token="${NEXDNS_Token:-$(_readaccountconf_mutable NEXDNS_Token)}"
NEXDNS_Api="${NEXDNS_Api:-$(_readaccountconf_mutable NEXDNS_Api)}"
if [ -z "$NEXDNS_Token" ]; then
_err "You have not set NEXDNS_Token yet."
_err "Create one at https://nexdns.tech/settings/api-keys, on a plan that includes API access, then:"
_err "export NEXDNS_Token=\"your-api-token\""
return 1
fi
if [ -z "$NEXDNS_Api" ]; then
NEXDNS_Api="$NEXDNS_Api_Default"
fi
#A trailing slash would make every request path begin with a double slash.
NEXDNS_Api="$(echo "$NEXDNS_Api" | sed 's|/*$||')"
_debug NEXDNS_Api "$NEXDNS_Api"
return 0
}
#_acme-challenge.www.example.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=example.com
# _domain_id=Zm9vYmFy
_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
#not valid
return 1
fi
if ! _nexdns_rest GET "zones?search=$h&per_page=100"; then
return 1
fi
#search matches on a substring, so the page can also hold zones that merely
#contain h. Take the id of the one whose name is exactly h.
_domain_id="$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
}
#Usage: _nexdns_rest GET|POST|DELETE path [body] [attempt]
_nexdns_rest() {
m=$1
ep=$2
data=$3
attempt=${4:-1}
_debug "$ep"
export _H1="Authorization: Bearer $NEXDNS_Token"
export _H2="Content-Type: application/json"
export _H3="Accept: application/json"
if [ "$m" = "GET" ]; then
response="$(_get "$NEXDNS_Api/$ep")"
else
_debug2 data "$data"
response="$(_post "$data" "$NEXDNS_Api/$ep" "" "$m" "application/json")"
fi
if [ "$?" != "0" ]; then
_err "error $ep"
return 1
fi
#A single certificate costs a handful of requests, but a renewal sweep over
#many of them meets the account's per-minute budget, and that run is
#unattended. Retry-After is treated as a floor: an api may report the time one
#token needs at an average rate and name a second when nothing frees for a
#minute, so the wait grows on its own across attempts.
if [ "$(grep "^HTTP" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" = "429" ]; then
if [ "$attempt" -ge 4 ]; then
_err "$m $ep failed: rate limited, and the wait budget is spent"
return 1
fi
_retry_after="$(grep -i "^Retry-After" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d : -f 2 | tr -d " \r\n")"
_backoff="$(_math "$attempt" \* 15)"
#The header may also carry an http date. Anything but a plain count of
#seconds falls through to the backoff rather than being parsed: guessing
#wrong about a date is worse than waiting a known interval, and comparing a
#date numerically would abort the hook outright.
case "$_retry_after" in
"" | *[!0-9]*) _retry_after="$_backoff" ;;
*)
if [ "$_retry_after" -lt "$_backoff" ]; then
_retry_after="$_backoff"
fi
;;
esac
#A wait longer than this is a refusal rather than a schedule, and sleeping
#it out would hold the hook for the length of the window. Hand the run back
#instead, so the next cron pass picks it up.
if [ "$_retry_after" -gt 120 ]; then
_err "$m $ep failed: rate limited for ${_retry_after}s, longer than this hook will wait"
return 1
fi
_info "Rate limited by the NexDNS API; retrying in $_retry_after seconds."
_sleep "$_retry_after"
_nexdns_rest "$m" "$ep" "$data" "$(_math "$attempt" + 1)"
return $?
fi
#Whitespace between a key and its value would defeat every match made on the
#body, here and in the callers.
response="$(echo "$response" | _normalizeJson)"
_debug2 response "$response"
#The status line decides success, not the body: a delete answers 204 with no
#body at all, and a record whose own content contains "error": would otherwise
#turn a stored value into a reported failure. The body is read only for the
#message once the status says the request was rejected.
_code="$(grep "^HTTP" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 _code "$_code"
case "$_code" in
"" | 2*)
return 0
;;
esac
#A rejected request carries {"error":{"code":..,"message":..}}, so say what the
#api says went wrong.
_message="$(echo "$response" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
if [ -z "$_message" ]; then
_message="status $_code"
fi
_err "$m $ep failed: $_message"
return 1
}

View File

@@ -227,7 +227,7 @@ _poweradmin_rest() {
return 1 return 1
fi fi
if printf '%s' "$response" | grep -q '"success"[[:space:]]*:[[:space:]]*false'; then if printf '%s' "$response" | grep -q '"success"[ ]*:[ ]*false'; then
_err "API reported failure on $method $ep" _err "API reported failure on $method $ep"
_debug "Response: $response" _debug "Response: $response"
return 1 return 1

View File

@@ -71,7 +71,7 @@ dns_rage4_rm() {
_debug "Getting txt records" _debug "Getting txt records"
_rage4_rest "getrecords/?id=${_domain_id}" _rage4_rest "getrecords/?id=${_domain_id}"
_record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -rn 's/.*"id":([[:digit:]]+),.*/\1/p') _record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -n 's/.*"id":\([0-9][0-9]*\),.*/\1/p')
if [ -z "$_record_id" ]; then if [ -z "$_record_id" ]; then
_err "error retrieving the record_id of the new TXT record in order to delete it, got: '$_record_id'." _err "error retrieving the record_id of the new TXT record in order to delete it, got: '$_record_id'."
return 1 return 1

145
dnsapi/dns_rltx.sh Normal file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_rltx_info='Realtox Media Cloudpanel DNS API
Site: realtoxmedia.de
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_rltx
Options:
RLTX_Key API Key
RLTX_OrganizationID Organization ID
'
######## Public functions #####################
#Usage: dns_rltx_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_rltx_add() {
fulldomain=$1
txtvalue=$2
_info "Using Realtox Media Cloudpanel DNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _rltx_init; then
return 1
fi
if ! _get_root "$fulldomain"; then
_err "Could not find matching DNS zone for $fulldomain"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
data="{\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"ttl\":120}"
if ! _rltx_rest POST "domains/$_domain_id/dns/acme-txt" "$data"; then
_err "Add TXT record request failed"
return 1
fi
if _contains "$response" '"status":"added"'; then
_info "Added TXT record, OK"
return 0
fi
_err "Add TXT record failed: $response"
return 1
}
#Usage: fulldomain txtvalue
#Remove the txt record after validation.
dns_rltx_rm() {
fulldomain=$1
txtvalue=$2
_info "Using Realtox Media Cloudpanel DNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _rltx_init; then
return 1
fi
if ! _get_root "$fulldomain"; then
_err "Could not find matching DNS zone for $fulldomain"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
data="{\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"ttl\":120}"
if ! _rltx_rest DELETE "domains/$_domain_id/dns/acme-txt" "$data"; then
_err "Remove TXT record request failed"
return 1
fi
if _contains "$response" '"status":"removed"'; then
_info "Removed TXT record, OK"
return 0
fi
_err "Remove TXT record failed: $response"
return 1
}
#################### Private functions below ##################################
_rltx_init() {
RLTX_Key="${RLTX_Key:-$(_readaccountconf_mutable RLTX_Key)}"
RLTX_OrganizationID="${RLTX_OrganizationID:-$(_readaccountconf_mutable RLTX_OrganizationID)}"
if [ -z "$RLTX_Key" ] || [ -z "$RLTX_OrganizationID" ]; then
RLTX_Key=""
RLTX_OrganizationID=""
_err "Please specify RLTX_Key and RLTX_OrganizationID."
_err "You can export them and retry: export RLTX_Key=... RLTX_OrganizationID=..."
return 1
fi
_saveaccountconf_mutable RLTX_Key "$RLTX_Key"
_saveaccountconf_mutable RLTX_OrganizationID "$RLTX_OrganizationID"
}
_get_root() {
domain=$1
fqdn_encoded="$(printf "%s" "$domain" | _url_encode)"
if ! _rltx_rest GET "domains/dns/acme-zone?fqdn=$fqdn_encoded"; then
return 1
fi
if ! _contains "$response" '"domain_id":"'; then
return 1
fi
_domain_id="$(printf "%s" "$response" | _egrep_o '"domain_id":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)"
_domain="$(printf "%s" "$response" | _egrep_o '"zone":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)"
_sub_domain="$(printf "%s" "$response" | _egrep_o '"record_name":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)"
if [ -z "$_domain_id" ] || [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then
return 1
fi
return 0
}
_rltx_rest() {
m=$1
ep="$2"
data="$3"
_debug "$ep"
export _H1="X-API-Key: $RLTX_Key"
export _H2="X-Organization-ID: $RLTX_OrganizationID"
export _H3="Content-Type: application/json"
if [ "$m" = "GET" ]; then
response="$(_get "https://api.ccp.realtoxmedia.de/api/$ep")"
else
_debug2 data "$data"
response="$(_post "$data" "https://api.ccp.realtoxmedia.de/api/$ep" "" "$m")"
fi
if [ "$?" != "0" ]; then
_err "Realtox Media Cloudpanel API request failed: $ep"
return 1
fi
_debug2 response "$response"
return 0
}

View File

@@ -368,7 +368,7 @@ _get_auth_token() {
_data_auth="{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":{\"user\":{\"name\":\"${SL_Login_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"},\"password\":\"${SL_Pswd}\"}}},\"scope\":{\"project\":{\"name\":\"${SL_Project_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"}}}}}" _data_auth="{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":{\"user\":{\"name\":\"${SL_Login_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"},\"password\":\"${SL_Pswd}\"}}},\"scope\":{\"project\":{\"name\":\"${SL_Project_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"}}}}}"
export _H1="Content-Type: application/json" export _H1="Content-Type: application/json"
_result=$(_post "$_data_auth" "$auth_uri") _result=$(_post "$_data_auth" "$auth_uri")
_token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | sed -nE "s/[[:space:]]*x-subject-token:[[:space:]]*([[:print:]]*)(\r*)/\1/p") _token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | cut -d ':' -f 2- | tr -d ' \t\r')
_dt_curr=$(date +%s) _dt_curr=$(date +%s)
SL_Token_V2="${SL_Login_Name}${_sl_sep}${_token_keystone}${_sl_sep}${SL_Login_ID}${_sl_sep}${SL_Project_Name}${_sl_sep}${_dt_curr}" SL_Token_V2="${SL_Login_Name}${_sl_sep}${_token_keystone}${_sl_sep}${SL_Login_ID}${_sl_sep}${SL_Project_Name}${_sl_sep}${_dt_curr}"
_saveaccountconf_mutable SL_Token_V2 "$SL_Token_V2" _saveaccountconf_mutable SL_Token_V2 "$SL_Token_V2"

View File

@@ -42,7 +42,10 @@ dns_selfhost_add() {
# only match full domains (at the beginning of the string or with a leading whitespace), # only match full domains (at the beginning of the string or with a leading whitespace),
# e.g. don't match mytest.example.com or sub.test.example.com for test.example.com # e.g. don't match mytest.example.com or sub.test.example.com for test.example.com
# if the domain is defined multiple times only the last occurance will be matched # if the domain is defined multiple times only the last occurance will be matched
mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain)(:[[:digit:]]+)([:]?[[:digit:]]*)(.*)/\2\3\4/p") # prepend a space to each line so "start of line" and "after whitespace"
# can both be matched as "after a space/tab" (portable BRE, no ERE (^|..))
_selfhost_tab="$(printf '\t')"
mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*:\{0,1\}[0-9]*\).*/\1/p")
_debug2 mapEntry "$mapEntry" _debug2 mapEntry "$mapEntry"
if test -z "$mapEntry"; then if test -z "$mapEntry"; then
_err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID" _err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID"
@@ -54,7 +57,7 @@ dns_selfhost_add() {
rid2=$(echo "$mapEntry" | cut -d: -f3) rid2=$(echo "$mapEntry" | cut -d: -f3)
# read last used rid domain # read last used rid domain
lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain:[[:digit:]]+)(.*)/\2/p") lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*\).*/\1/p")
_debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry" _debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry"
lastUsedRidForDomain=$(echo "$lastUsedRidForDomainEntry" | cut -d: -f2) lastUsedRidForDomain=$(echo "$lastUsedRidForDomainEntry" | cut -d: -f2)

View File

@@ -145,8 +145,8 @@ _udr_rest() {
_debug data "${data}" _debug data "${data}"
response="$(_post "${data}" "${UDR_API}?s_login=${UDR_USER}&s_pw=${UDR_PASS}" "" "POST")" response="$(_post "${data}" "${UDR_API}?s_login=${UDR_USER}&s_pw=${UDR_PASS}" "" "POST")"
_code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') _code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | tr -d ' \t\r')
_description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') _description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | tr -d '\r' | sed -e 's/^[ ]*//' -e 's/[ ]*$//')
_debug response_code "$_code" _debug response_code "$_code"
_debug response_description "$_description" _debug response_description "$_description"

View File

@@ -61,7 +61,7 @@ dns_world4you_add() {
if _contains "$res" "successfully"; then if _contains "$res" "successfully"; then
return 0 return 0
else else
msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g') msg=$(_w4y_alert_msg "$res")
if [ "$msg" = '' ]; then if [ "$msg" = '' ]; then
_err "Unable to add record: Unknown error" _err "Unable to add record: Unknown error"
echo "$ret" >'error-01.html' echo "$ret" >'error-01.html'
@@ -125,7 +125,7 @@ dns_world4you_rm() {
if _contains "$res" "successfully"; then if _contains "$res" "successfully"; then
return 0 return 0
else else
msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g') msg=$(_w4y_alert_msg "$res")
if [ "$msg" = '' ]; then if [ "$msg" = '' ]; then
_err "Unable to remove record: Unknown error" _err "Unable to remove record: Unknown error"
echo "$ret" >'error-01.html' echo "$ret" >'error-01.html'
@@ -145,6 +145,17 @@ dns_world4you_rm() {
################ Private functions ################ ################ Private functions ################
# Usage: _w4y_alert_msg <html>
# Extracts the error text out of the alert box of a DNS page.
# "grep -A" is not portable (Solaris /usr/bin/grep: "illegal option -- A"),
# so select from the alert to EOF and keep the same number of lines.
# "\s" is a GNU sed extension, use an explicit space/tab bracket instead.
_w4y_alert_msg() {
_w4y_tab=$(printf '\t')
echo "$1" | sed -n '/alert-notification/,$p' | _head_n 21 |
grep 'class="weak-title">[^<]' | sed "s/<[^>]*>//g;s/^[ $_w4y_tab]*//"
}
# Usage: _login # Usage: _login
_login() { _login() {
WORLD4YOU_USERNAME="${WORLD4YOU_USERNAME:-$(_readaccountconf_mutable WORLD4YOU_USERNAME)}" WORLD4YOU_USERNAME="${WORLD4YOU_USERNAME:-$(_readaccountconf_mutable WORLD4YOU_USERNAME)}"

View File

@@ -149,7 +149,7 @@ _check_variables() {
org_response="$(echo "$org_response" | _normalizeJson)" org_response="$(echo "$org_response" | _normalizeJson)"
YANDEX360_ORG_ID=$( YANDEX360_ORG_ID=$(
echo "$org_response" | echo "$org_response" |
_egrep_o '"id":[[:space:]]*[0-9]+' | _egrep_o '"id":[ ]*[0-9]+' |
cut -d':' -f2 cut -d':' -f2
) )
_debug 'Automatically retrieved YANDEX360_ORG_ID' "$YANDEX360_ORG_ID" _debug 'Automatically retrieved YANDEX360_ORG_ID' "$YANDEX360_ORG_ID"
@@ -216,7 +216,7 @@ _get_token() {
interval=$( interval=$(
echo "$response" | echo "$response" |
_egrep_o '"interval":[[:space:]]*[0-9]+' | _egrep_o '"interval":[ ]*[0-9]+' |
cut -d':' -f2 cut -d':' -f2
) )
_debug 'Polling interval' "$interval" _debug 'Polling interval' "$interval"

View File

@@ -22,21 +22,32 @@ dns_yc_add() {
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
txtvalue=$2 txtvalue=$2
# YC_SA_Key_File_PEM_b64/Path are always persisted to the domain conf below,
# so they must be recovered from there first (account conf is only a
# fallback for the YC_Folder_ID case, see the SA_ID/SA_Key_ID save below).
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}"
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}" YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}"
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}"
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}" YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}"
if [ "$YC_SA_Key_File_PEM_b64" ]; then if [ "$YC_SA_Key_File_PEM_b64" ]; then
echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key
YC_SA_Key_File="private.key" YC_SA_Key_File="private.key"
_yc_key_is_temp=1
_savedomainconf YC_SA_Key_File_PEM_b64 "$YC_SA_Key_File_PEM_b64" _savedomainconf YC_SA_Key_File_PEM_b64 "$YC_SA_Key_File_PEM_b64"
else else
YC_SA_Key_File="$YC_SA_Key_File_Path" YC_SA_Key_File="$YC_SA_Key_File_Path"
_yc_key_is_temp=""
_savedomainconf YC_SA_Key_File_Path "$YC_SA_Key_File_Path" _savedomainconf YC_SA_Key_File_Path "$YC_SA_Key_File_Path"
fi fi
YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}"
YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}" YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}"
YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}"
YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}" YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}"
YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}"
YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}" YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}"
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}"
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}" YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}"
if [ "$YC_SA_ID" ] && [ "$YC_SA_Key_ID" ] && [ "$YC_SA_Key_File" ]; then if [ "$YC_SA_ID" ] && [ "$YC_SA_Key_ID" ] && [ "$YC_SA_Key_File" ]; then
@@ -65,11 +76,21 @@ dns_yc_add() {
return 1 return 1
fi fi
else else
# Clear both possible stores -- YC_Zone_ID/YC_Folder_ID/key material are
# persisted to the domain conf, while YC_SA_ID/YC_SA_Key_ID may have been
# saved account-wide (Folder_ID mode), so a plain _clearaccountconf alone
# would leave stale values behind in whichever store wasn't touched.
_cleardomainconf YC_Zone_ID
_clearaccountconf YC_Zone_ID _clearaccountconf YC_Zone_ID
_cleardomainconf YC_Folder_ID
_clearaccountconf YC_Folder_ID _clearaccountconf YC_Folder_ID
_clearaccountconf YC_SA_ID _cleardomainconf YC_SA_ID
_clearaccountconf YC_SA_Key_ID _clearaccountconf_mutable YC_SA_ID
_cleardomainconf YC_SA_Key_ID
_clearaccountconf_mutable YC_SA_Key_ID
_cleardomainconf YC_SA_Key_File_PEM_b64
_clearaccountconf YC_SA_Key_File_PEM_b64 _clearaccountconf YC_SA_Key_File_PEM_b64
_cleardomainconf YC_SA_Key_File_Path
_clearaccountconf YC_SA_Key_File_Path _clearaccountconf YC_SA_Key_File_Path
_err "You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File." _err "You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File."
return 1 return 1
@@ -110,11 +131,30 @@ dns_yc_rm() {
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
txtvalue=$2 txtvalue=$2
YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}"
YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}" YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}"
YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}"
YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}" YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}"
YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}"
YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}" YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}"
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}"
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}" YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}"
# See dns_yc_add() for why domain conf is checked before account conf.
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}"
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}"
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}"
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}"
if [ "$YC_SA_Key_File_PEM_b64" ]; then
echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key
YC_SA_Key_File="private.key"
_yc_key_is_temp=1
else
YC_SA_Key_File="$YC_SA_Key_File_Path"
_yc_key_is_temp=""
fi
_debug "First detect the root zone" _debug "First detect the root zone"
if ! _get_root "$fulldomain"; then if ! _get_root "$fulldomain"; then
_err "invalid domain" _err "invalid domain"
@@ -124,16 +164,10 @@ dns_yc_rm() {
_debug _sub_domain "$_sub_domain" _debug _sub_domain "$_sub_domain"
_debug _domain "$_domain" _debug _domain "$_domain"
_debug "Getting txt records" # upsertRecordSets.deletions removes only the given value from the rrset,
if _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then # leaving any other values at the same name (e.g. base + wildcard domain)
exists_txtvalue=$(echo "$response" | _normalizeJson | _egrep_o "\"data\".*\][^,]*" | _egrep_o "[^:]*$") # intact -- no need to read the current data set and recompute it.
_debug exists_txtvalue "$exists_txtvalue" if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
else
_err "Error: $response"
return 1
fi
if _yc_rest POST "zones/$_domain_id:updateRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":$exists_txtvalue}]}"; then
if _contains "$response" "\"done\": true"; then if _contains "$response" "\"done\": true"; then
_info "Delete, OK" _info "Delete, OK"
return 0 return 0
@@ -194,7 +228,7 @@ _get_root() {
return 1 return 1
fi fi
if _contains "$response" "\"zone\": \"$h\""; then if _contains "$response" "\"zone\": \"$h\""; then
_domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:]*$" | tr -d '"') _domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')
_debug _domain_id "$_domain_id" _debug _domain_id "$_domain_id"
if [ "$_domain_id" ]; then if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
@@ -255,7 +289,9 @@ _yc_login() {
_signature=$(printf "%s.%s" "$header" "$payload" | _sign "$YC_SA_Key_File" "sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1" | _url_replace) _signature=$(printf "%s.%s" "$header" "$payload" | _sign "$YC_SA_Key_File" "sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1" | _url_replace)
_debug2 _signature "$_signature" _debug2 _signature "$_signature"
rm -rf "$YC_SA_Key_File" if [ "$_yc_key_is_temp" ]; then
rm -f "$YC_SA_Key_File"
fi
_jwt=$(printf "{\"jwt\": \"%s.%s.%s\"}" "$header" "$payload" "$_signature") _jwt=$(printf "{\"jwt\": \"%s.%s.%s\"}" "$header" "$payload" "$_signature")
_debug2 _jwt "$_jwt" _debug2 _jwt "$_jwt"
@@ -264,7 +300,7 @@ _yc_login() {
_iam_response="$(_post "$_jwt" "https://iam.api.cloud.yandex.net/iam/v1/tokens" "" "POST")" _iam_response="$(_post "$_jwt" "https://iam.api.cloud.yandex.net/iam/v1/tokens" "" "POST")"
_debug3 _iam_response "$(echo "$_iam_response" | _normalizeJson)" _debug3 _iam_response "$(echo "$_iam_response" | _normalizeJson)"
YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:]*$" | tr -d '"')" YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')"
_debug3 YC_Token _debug3 YC_Token
return 0 return 0

View File

@@ -57,9 +57,8 @@ waha_send() {
_debug "_data" "$_data" _debug "_data" "$_data"
export _H1="Content-Type: application/json"
if [ "$WAHA_API_KEY" ]; then if [ "$WAHA_API_KEY" ]; then
export _H2="X-Api-Key: $WAHA_API_KEY" export _H1="X-Api-Key: $WAHA_API_KEY"
fi fi
_waha_url="${WAHA_URL}/api/sendText" _waha_url="${WAHA_URL}/api/sendText"