Compare commits

...
40 Commits
Author SHA1 Message Date
neil c9e9e24e24 _json_encode: fix empty output on Solaris, newline loss in _hex_dump fallback
_hex_dump strips the trailing newline, and Oracle Solaris sed drops an
unterminated last line, so the sed step in _json_encode produced nothing
and every caller got an empty string on Solaris. Capture the hex first and
re-emit it with printf "%s\n".

The _ascii_hex fallback of _hex_dump (used when od/hexdump/xxd are not in
PATH, e.g. Tribblix) captured stdin with $(cat), losing trailing newlines,
and ran cut -c per line, so multi-line input was mis-encoded. Process the
input line by line instead.

Both surfaced by le_test_json_encode (#7212).
2026-10-21 20:22:33 +08:00
neil aef81df5eb dns_gandi_livedns: delete the TXT record when rm removes its last value
rm PUT the remaining values back even when none were left, leaving an
empty TXT rrset behind. Use DELETE (204 on success) in that case; keep
PUT when other values remain, since the rrset can hold values from
other pending validations.

Refs #7347
2026-10-21 18:49:59 +08:00
neil e3004b84a1 dns_gandi_livedns: load saved credentials in rm
Since 329dab9a (3.1.3) the token/key is saved with
_saveaccountconf_mutable, but only _add read it back. rm runs in its
own subshell, so on cron renewal it called the API unauthenticated;
the 401 body has no "code": 401, so _get_root took the first probe as
the zone and the TXT record was never removed.

Fixes #7347
2026-10-21 17:52:45 +08:00
neil 3288c35c2a _json_encode/_json_decode: use printf instead of echo
dash's echo interprets backslash escapes, so the \. MarkdownV2 escapes
built by notify/telegram.sh collapsed to \. in the JSON body and
Telegram rejected the message with "Character '.' is reserved".
bash's echo does not, which is why only dash-based systems were hit.

Fixes #7212
2026-10-21 17:52:08 +08:00
5d07634e0f dns_namecheap: let NAMECHEAP_* env vars override saved credentials (#7344)
Co-authored-by: zorba <zorba-claude@pavlovian.net>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 14:09:31 +08:00
a57c8a45fa Add hp_ews deploy hook for HP printers (#7198)
* Add hp_ews deploy hook for HP printers

Deploys the certificate to an HP printer's Embedded Web Server by building
a PKCS#12 bundle from the key, certificate and CA chain and POSTing it to
/Security/DeviceCertificates/NewCertWithPassword/Upload.

Tested on an HP LaserJet Pro MFP M479fdw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* hp_ews: upload with _post_file and Basic auth

Replace the hand-rolled curl/wget helper with _post_file, as requested
in review. Authenticate with a Basic Authorization header like the other
deploy hooks, and map HP_EWS_INSECURE onto HTTPS_INSECURE, which stays
inside the hook's subshell. Printers that only offer Digest auth are no
longer supported.

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

* hp_ews: declare and export _H1 separately (SC2155)

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

---------

Co-authored-by: Sean Graham <sean.graham@vizit.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-19 15:23:35 +08:00
Ilya Vassyutovich 0d1d0092b8 dns_yc: use absolute record names (#7338)
Record names were sent relative to the zone, derived from _sub_domain.
That has no valid form when the TXT name is the zone apex, which
happens with --domain-alias pointing at a dedicated zone:

- YC_Zone_ID: _sub_domain ends up empty and the API rejects the
  request ("Length must be in the range of 1 to 254").
- YC_Folder_ID: _get_root returns the first label as _sub_domain, so
  the record lands at <alias>.<alias>. and validation never succeeds.

The API accepts absolute names (trailing dot) everywhere a relative one
is accepted, so send $fulldomain, which is already lowercased and
dot-terminated. Ordinary _acme-challenge.<sub> names keep working.

Supersedes #6862.
2026-10-18 14:34:17 +08:00
Voyo c472636bae Merge pull request #7333 from voyo/add-dns-primedns
Add PrimeDNS DNS API (dns_primedns)
2026-10-16 20:50:38 +08:00
579c994fad Handle DNSimple API response for nonexistent subdomain (#7324)
* handle OAuth response

* revert comment removal

* handle lowercased domain value

* expand error detection logic for all non-expected responses

* shell-independent lowercasing

* revert; domain value already lowercased

* add missing double-quotes

* use zone records endpoint fallback when zone cannot be identified

* add logic missing from previous

---------

Co-authored-by: neil <github@neilpang.com>
Co-authored-by: Mashiro <adadam@qq.com>
Co-authored-by: MBWhitestone <25477219+MBWhitestone@users.noreply.github.com>
Co-authored-by: Pablo <Pablo1@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-16 16:11:30 +08:00
neil f659ba2ea2 Skip conf writes that change nothing
Since #7278 every conf write is a temp file plus a rename, and _setopt
and _clear_conf rewrote the conf even when the content stayed the same:
_savedeployconf clears a legacy key that is almost never there, and
deploy hooks re-save the same values on every run. On storage where
file creation and rename are expensive (ZFS on HDD) a multideploy run
went from about 23 seconds to over 2 minutes.

Return early when the new content equals the old one. A simulated
3-service docker multideploy drops from 96 conf writes to 32.

Fixes #7335
2026-10-15 15:03:18 +08:00
neil f5f2242d60 deploy/ssh: stop the batched remote command at the first failed step
In single-call mode (the default) all steps were joined with ";", so a
failed write did not stop the batch: the remaining files were still
overwritten, DEPLOY_SSH_REMOTE_CMD still ran on the broken file, and the
deploy reported success because only the last exit code counted.

With a POSIX remote shell, steps are now wrapped in { } and joined with
&&, matching multi-call mode. Non-POSIX remote shells (powershell -c,
cmd /c) keep the old ";" join byte for byte.

See #7328
2026-10-14 14:13:21 +08:00
AmadeusAbionandstelar7 d73d68ced4 Add Abion.com DNS API (#7311)
* Add Abion.com DNS API

* Update dns_abion.sh

* dns_abion: keep existing TXT records on add and rm

Continues acmesh-official/acme.sh#6885 by @stelar7 and addresses
the review comments there.

- add: append to the existing TXT values at the name instead of
  replacing them, so wildcard + base certificates work
- rm: remove only the matching value, keep the TTL and comments of
  the other records, and clear the name with null when it is empty
- only read the TXT records of the target name; the old parser also
  picked up TXT records from other names in the zone
- find the zone with GET zones/<name> instead of the paginated zone
  list, and stop with the API error on anything but "Zone not found"
- add Docs line and progress messages, fix shfmt

Co-authored-by: stelar7 <dudedbz@gmail.com>

* dns_abion: lowercase the domain so rm finds the record

* dns_abion: add tracking issue to info block

* dns_abion: align info block with the structural info format

---------

Co-authored-by: stelar7 <dudedbz@gmail.com>
2026-10-14 12:39:44 +08:00
Eyal Kalderon 228a140094 Add deploy hook for Redfish-compatible BMCs (#7276)
* Add `deploy/redfish.sh` and implement Redfish session log in/out

Since `acme.sh` intends to keep third-party dependencies to a minimum,
this implementation will not rely on `redfishtool`.

* Detect BMC manager and replace existing cert with ACME cert

* Reuse `_toPkcs8` defined in `acme.sh`

* Return early if unsupported key algorithms or bit lengths are used

* Improve robustness and UX a bit

* Support opt-in automatic restart of BMC

* Improve log output and inline docs some more

* Remove `test-redfish-deploy-hook.sh` test harness

The business logic has been implemented, so it should be safe to delete
this test harness now and just interact with `acme.sh` directly.

* Trim `_redfish_` prefixes from local variables

Since each deploy hook is executed within its own subshell, there's no
risk of polluting the outer `acme.sh` environment nor any other deploy
hooks that might follow.

* Save deploy configuration changes to disk

* Tweak deploy hook log messages a bit

* Add `DEPLOY_REDFISH_MANAGER` environment variable

* Add function for checking HTTP status code

* Add more helper functions, clean up log output a bit more

* Return early from non-zero `curl` exit codes

* Only upload leaf cert in `--sign-csr` mode

* Fix bug with RSA key compatibility error message

* Remove `_redfish_load_deploy_conf` helper, use consistent var naming

* Add several more helper functions

* Check whether server supports Certificate schema >=1.4.0

Redfish servers with a schema version older than this don't support
uploading private keys + full certificate chains directly. Instead, the
CSR must be generated on the server and signed with `acme.sh --sign-csr`
prior to deployment.

* Clarify environment variable naming a bit

* Don't implicitly opt into `HTTPS_INSECURE`

* Feedback: Fix lower-bound RSA key length check

* Feedback: Verify file existence with `-f` flag

* Feedback: Correctly map `TPM_ECC_CURVE` key length values

* Feedback: Replace exact HTTP 204 checks with 2xx instead

* Feedback: Use POSIX-compliant alternative to `\+`

* Feedback: Get session token from body instead of HTTP header

* Feedback: Save PKCS#8 private key directly to variable

* Feedback: Drop `jq` dependency, handle missing action info

* Fix regression in `_redfish_rest()` error reporting

* Simplify construction of expected certificate JSON string

Confirmed with a real-world test that the value of `_certificate_str`
is unchanged before and after applying this commit. See below:

diff --git a/deploy/redfish.sh b/deploy/redfish.sh
index e485fd60..3459b71a 100644
--- a/deploy/redfish.sh
+++ b/deploy/redfish.sh
@@ -131,6 +131,10 @@ redfish_deploy() {
       printf '%s\n' "${_ckey_pkcs8}"
       cat "${_cfullchain}"
     } | _json_encode)"
+
+    _new_certificate_str="$(echo "${_ckey_pkcs8}" | cat - "${_cfullchain}" | _json_encode)"
+    [ "${_certificate_str}" = "${_new_certificate_str}" ] || return 123
+
     _certificate_type='PEMchain'
   else
     _info "Private key and full certificate chain not available; we must be using a server-generated CSR."

* Include link to `redfish-generate-csr.sh` helper script in error message

* Feedback: Drop use of `paste` and `expr`

* Feedback: Remove stale `_err` invocation

* Feedback: Correctly deal with whitespace after colon

* Feedback: Handle `\n` in username and password in Dash shell

It seems that `echo` in Dash automatically converts `\n` into a line
break, causing usernames and/or passwords containing a literal
backslash-n to reach the BMC with a newline, breaking login. It
seems that `printf '%s'` doesn't have the same issue, so we use those to
format `_username` and `_password` instead of `echo`.

* Feedback: Link to acme.sh wiki instead of Gist

* Feedback: Fix `Certificate` schema version check

* Feedback: Better handle missing key algo/length/curve info

This commit changes how the deploy hook detects which cryptographic
algorithms are supported by the server if not explicitly declared in
`KeyPairAlgorithm`. Instead of blindly assuming RSA, we instead check
for the presence of `KeyBitLength` and/or `KeyCurveId` fields and try to
make an educated guess. If not enough info exists to determine key
compatibility, we fail-open instead of failing closed; the server will
yell at us anyway if `ReplaceCertificate` doesn't support something.

* Feedback: Fix character escaping of username/password

* Feedback: Correctly parse inline `KeyBitLength@Redfish.AllowableValues`

This metadatum is represented as a JSON array of allowed values, not a
numeric range with min/max. Make sure to treat the element in the list
as the maximum, and not assume the list is at most 2 elements long.

* Feedback: Add "\n" to format string for Solaris compatibility

* Feedback: Use POSIX parameter expansion instead of `rev`
2026-10-14 12:38:33 +08:00
Kevin Winter f5aeed1752 dns_cf: support IDN domains (#7322)
* dns_cf: support IDN domains (punycode record lookup, URL-encoded zone lookup)

* dns_cf: convert IDN once at the top of dns_cf_add/dns_cf_rm
2026-10-14 12:37:08 +08:00
Lee 81fd2d2126 Add Option to Control HTTP Scheme (#7318)
* Add Option to Control HTTP Scheme

Once certificates are installed renewal should always use HTTPS.
HTTP is only required if there have never been any certificates on
the device.

This also fixes an authentication issue where HTTPS is forced
by the device. Before this patch authentication failed for
devices which forced HTTPS because the POST request used
for request did not follow the HTTP to HTTPS redirect.

* Use http as Default Scheme

Using https as default for SHELLY_SCHEME breaks existing deployments
as they may not have a SHELLY_HOST which is identical to a name in
the certificate.

SHELLY_SCHEME is now a parameter as any other without the automatic
upgrade to https. This will always need to be done manually.
2026-10-13 09:43:28 +08:00
Tom BeardandTom Beard 772ed2aa93 Add Public Internet DNS API (dns_pblin) (#7316)
Co-authored-by: Tom Beard <12370027+tombeard@users.noreply.github.com>
2026-10-12 17:37:00 +08:00
Marvo2011andneil ee38f28e13 Update SelfHost DNS API (#7307)
* Changes for new selfhost api

* Skip acmetestXyzRandomName test, selfhost does not longer support dns updates to other recods than _acme-challenge.

* Add selfhost migration hint and instructions

---------

Co-authored-by: neil <github@neilpang.com>
2026-10-12 17:33:00 +08:00
Matteo Gaggiano 9d09edcd69 [dnsapi] add MVMNET dns api (#6606)
* feat: add initial version of MVMNET dns api

* fix: adjust shell format with shfmt

* feat: add initial version of MVMNET dns api

* fix: adjust shell format with shfmt

* Change runner from ubuntu-latest to self-hosted

* Revert "Change runner from ubuntu-latest to self-hosted"

This reverts commit 55eb217fa1.

* fix(dns_mvmnet): use configurable API endpoint

* fix(dns_mvmnet): extract records portably

* fix(dns_mvmnet): namespace private helpers

* fix(dns_mvmnet): stop on missing credentials

* fix(dns_mvmnet): reuse normalized subdomain

* fix(dns_mvmnet): reuse validated credentials

* docs(dns_mvmnet): update API and issue links
2026-10-12 17:30:59 +08:00
neil cf51ad22d1 dns_baidu, dns_bhosted: pipe input to _lower_case, not an argument
_lower_case reads stdin only. Called as _lower_case "$x", the argument
was dropped and tr read the inherited stdin. On an interactive tty that
never reaches EOF, so dns_baidu hung forever on every add/rm (#7319).
Under cron stdin is /dev/null and the value came back empty: dns_baidu
fell into the default engine order by luck, and the dns_bhosted
fallback error checks could never match.

Fixes #7319
2026-10-12 17:02:10 +08:00
neil 3e6d4835e5 Retry signed requests on transport errors
_send_signed_request tested $? after _CACHED_NONCE="" had reset it, so a
POST that never got an answer (curl 7, 28, 35) returned success with a
stale or empty code, and callers blamed the CA ("Finalize code was not
200"). Keep the _post status and retry with backoff and a new nonce, like
a gateway error. Drop the nonce-fetch $? check, which was dead the same
way.

Fixes #7320
2026-10-12 16:58:32 +08:00
neil 0e4d75325b dns_yc: accept compact JSON responses (#7300)
The Yandex Cloud DNS API now returns compact JSON ("done":true,
"zone":"..."), so the add/rm success checks and the folder zone lookup
never matched. Also fix the zone id lookup when a zone has labels, and
strip the trailing dot from the record name in YC_Zone_ID mode.
2026-10-09 17:40:42 +02:00
fhaala 04c3e64ea7 Add optional SYNO_USE_TOKEN switch for Synology SRM compatibility (#7290)
* Add optional SYNO_USE_TOKEN switch for Synology SRM compatibility

* Fix Synology SRM token switch review comments

* Fix SYNO_USE_TOKEN handling
2026-10-09 17:28:32 +02:00
invario 2f8a5535dc Add deploy hook for nginx proxy manager and NPM plus (#7267)
* Add deploy hook for nginx proxy manager and npmplus

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* rename to nginx-proxy-manager
use _post instead of raw curl
support both npmplus and nginx API
create certificate if it doesn't already exist
json_encode values
evaluate and report server return code

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* - removed jq dependency, replaced with _egrep_o
- fix _secure_debug output for _npm_token
- remove newline from _npm_certname_json
- remove unused variables
- escape backslashes in passwords
- unset _H1 before returning
- tested on nginx proxy manager (image jc21/nginx-proxy-manager:latest)
- tested on NPMplus (image zoeyvid/npmplus:2026-07-24-r1)

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* add "requiresTotp" check for 2FA enablement (npmplus:develop branch)

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* remove api/schema detection and infer API based on token location

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* replaced `[[:space:]]`
evaluate `requires_2fa` and `requires_Totp` separately
unset `_H1` on additional return for failure
prefix `nl` `_boundary` `_payload`
add `_npm_payload_format` to strip backslashes, encode for JSON, and remove newlines

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* remove backslash handling from _npm_payload_format
use _utc_date instead of date

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

---------

Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2026-10-09 17:20:39 +02:00
b2e6173b1a fix: poweradmin dnsapi leaves hanging txt records (#7232)
* fix: improved record txt record matching

matching txt records sometimes failed if they contained certain chars causing leftover challenges in dns zone

* fix(dns_poweradmin): use portable record and zone matching

grep -o and grep -F are not supported by Solaris /usr/bin/grep.
Split JSON objects with the core _egrep_o wrapper and match name,
type and content with case, which is literal by construction. Loop
over a here-doc since case inside $(...) breaks bash 3.2 and
Solaris sh. Apply the same to _get_root.

Drop _json_escape: hostnames and base64url challenge values cannot
contain quotes or backslashes, so escaping was a no-op.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: neil <github@neilpang.com>
Co-authored-by: Jakob Næss <jakob@itsjefen.no>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 07:48:58 +02:00
neil acbe85b7e9 Support wget2 in the wget code path
Fedora 40 and later ship wget2 as wget. Under -q it prints nothing for -S,
so $HTTP_HEADER stayed empty in wget mode (ACME_USE_WGET=1, or no curl):
no status code, no Replay-Nonce, no Location, and every ACME request
failed. Without -q its own "HTTP response 200 OK [url]" line would be read
as the status line, and a HEAD response gets no headers at all.

Detect wget2 by its --version line and run it without -q (and without -d,
whose demultiplexing expects wget 1.x output). _wget2_headers keeps only
the header blocks of its stderr; for a response without a body, where
wget2 prints no block, it rebuilds the status line from the "HTTP
response" line. HEAD goes through --method HEAD --save-headers, which
writes the headers into $HTTP_HEADER. wget 1.x and curl are unchanged.
2026-09-27 19:32:38 +02:00
Tao AnandClaude Opus 5.5 6467ca9764 fix: keep the conf intact when writing it fails (#7247) (#7278)
* fix: keep the conf intact when writing it fails

_setopt() and _clear_conf() redirected the new content straight into the
conf file. The redirect truncates the file before anything is written,
so a failed write (no space left on device, quota, I/O error) left a
0-byte conf, _save_conf still returned 0, and the cert could not be
renewed any more even after space was freed.

Write the new content to a temp file next to the conf, verify it, then
rename it over the conf. The temp file is created with cp -p so the mode
and owner are kept. A symlinked or bind mounted conf cannot be renamed
over, so it is written in place once the temp copy is known to be good.
A failed write now returns 1 with an error and the conf untouched.

Fixes #7247

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL9G1DHE5yNVtkm3FFPPNX

* fix: route the append through _write_conf and keep the temp file on a failed in-place write

The append branch of _setopt() still wrote into the conf with >>, so a
write cut short (full disk, quota) left a half written line while
_setopt returned 0, and sourcing the conf failed afterwards. Build the
new content in all three branches and write it once through _write_conf.
This also drops the separate trailing newline append.

In the symlink / bind mount fallback, a failed in-place write leaves the
conf truncated. Keep the temp file in that case, since it is the only
complete copy, and name it in the error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7ohY7h4RUSNJ2ES14NMmY

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 10:02:42 +02:00
ngehrsitz a1de626512 Use UTF-16LE encoding in Fritzbox script (#7288)
instead of the glibc specifc UTF16LE alias that does not exist libiconv
2026-09-27 09:53:49 +02:00
Marc-Pascal ClementandClaude Opus 5.5 84a8d2a752 Add Healthchecks.io notify hook (#7282)
* Add Healthchecks.io notify hook

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

* notify/healthchecks: require an exact "OK" response

hc-ping.com answers HTTP 200 "OK (not found)" for an unknown UUID, so the
prefix match reported success for a mistyped or deleted check, including
the --set-notify test ping. Only treat an exact "OK" body as success.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 09:51:04 +02:00
neil 6ef16ffe63 Save CA_EMAIL only when the CA took the contact
_regAccount wrote CA_EMAIL before sending newAccount, so it recorded an
address the CA never stored: for an account key it already knows, the CA
answers 200 and ignores the contact of the request. A --register-account
-m on an existing account then left the local config claiming an email
the account does not have.

Save it in the 201 branch, and in the 200/409 branch say that the email
was not changed and point at --update-account -m. Nothing is printed
when the given address already matches the saved one.
2026-09-26 12:48:52 +02:00
neil 427787cb8f Guard the account email in __initHome too
_process() calls __initHome directly once the option loop is over, and
that sourcing of account.conf overwrote the address -m had just
exported -- before any command function reaches _initpath. Guarding
only _initpath was not enough: by then ACCOUNT_EMAIL already held the
saved address, so it was saved and restored unchanged.

Keep the live value across this sourcing as well.
2026-09-26 12:38:43 +02:00
neil 31f06a4ecf Keep the command line account email out of account.conf's way
_initpath sources account.conf (twice, counting __initHome), and the
assignment overwrote the ACCOUNT_EMAIL that -m had just exported, so
--update-account -m sent the saved address instead of the given one.
It also short-circuited _getAccountEmail, whose first branch is meant
for the live value: the saved global address then won over the per-CA
CA_EMAIL as well.

Save the live value before the sourcing and restore it after. The saved
address is still reached, by the _readaccountconf at the end of
_getAccountEmail, which is where it belongs in the order.
2026-09-26 12:14:17 +02:00
neil 6618d55bfc Decide the ARI replaces retry on the HTTP status, not on the CA's wording
The retry was keyed on the message text and missed ZeroSSL, which answers
HTTP 401 with 'The "replaces" field does not identify a certificate that
belongs to this ACME account', so switching the ACME server failed every
renewal.

RFC 9773 Section 5 mandates a problem type only for the 409
"alreadyReplaced" case and leaves its other checks (same account, shared
identifier) to server policy, so the wording cannot be enumerated. Judge
the status code instead: _isARIReplacesRejected treats only a non-2xx
response as a rejection. That also removes a false positive on the
success path, where the server "MUST reflect that field in the response"
and the echoed base64url certID can itself contain "ARI".

fix #7280
2026-09-26 11:50:22 +02:00
neil 879c1519bb Merge pull request #7263 from kekropp/dns_bergdns
Add bergdns.at DNS API
2026-09-26 11:41:07 +02:00
Kenny Kropp d51d853ad4 fix default value handling 2026-09-21 11:53:09 +02:00
neil ceed0e0228 start 3.1.7 2026-09-20 16:56:43 +02:00
neil aafc4efe1f allowed_signers: point the examples at 3.1.6
The comment block told the reader to run "git verify-tag 3.1.5", which is
exactly the tag that fails: 3.1.5 was tagged by the GitHub release form as
a lightweight ref and carries no signature. Use 3.1.6 in the examples and
state the baseline, so the file that teaches verification does not hand out
a command that cannot work.

https://github.com/acmesh-official/acme.sh/issues/7273
2026-09-20 13:13:20 +02:00
neil 138e0dff84 Move the release signing baseline to 3.1.6 and catch lightweight tags in CI
The 3.1.5 tag was created server-side by publishing the GitHub release,
which can only produce a lightweight ref, so it carries no signature and
git verify-tag fails on it. Rather than force-move a published tag, state
that signing starts at 3.1.6 and cut that tag locally with git tag -s
before the release is published.

vtag.yml now fails the run when the pushed tag is not a tag object, so the
same mistake shows up as a red check on the release instead of arriving as
a user report. The mirror step runs first, so v<tag> is still created; its
early "already exists" return became an else branch so the check is never
skipped.

https://github.com/acmesh-official/acme.sh/issues/7273
2026-09-20 13:06:37 +02:00
Kenny Kropp 84d6b5841e fix pr comment issues 2026-09-19 11:29:08 +02:00
Kenny Kropp 0ee2720d73 add bergdns.at provider info block values 2026-09-16 08:30:13 +02:00
Kenny Kropp e4c0a4f5f5 add bergdns.at support 2026-09-15 14:56:59 +02:00
26 changed files with 2597 additions and 170 deletions

No files matched your search

+30 -7
View File
@@ -6,6 +6,9 @@ name: Mirror version tag
# pointing to the same object, so both forms exist. # pointing to the same object, so both forms exist.
# No retrigger loop: the tag filter never matches a "v"-prefixed tag, and # No retrigger loop: the tag filter never matches a "v"-prefixed tag, and
# refs created with GITHUB_TOKEN do not fire workflows anyway. # refs created with GITHUB_TOKEN do not fire workflows anyway.
# The job mirrors first and then fails when the pushed tag is lightweight,
# which is what the release form produces: that tag can never carry a
# signature, so the failure has to be loud.
on: on:
push: push:
@@ -26,19 +29,39 @@ jobs:
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }} TAG: ${{ github.ref_name }}
run: | 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 # Mirror the object the pushed tag actually points at: the commit
# for a lightweight tag, the tag object itself for an annotated or # for a lightweight tag, the tag object itself for an annotated or
# signed one. Pointing the mirror at the commit would strip the # signed one. Pointing the mirror at the commit would strip the
# signature, so "git verify-tag v3.1.3" would fail while # signature, so "git verify-tag v3.1.3" would fail while
# "git verify-tag 3.1.3" succeeds. # "git verify-tag 3.1.3" succeeds.
sha="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq .object.sha)" _ref="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq '.object.sha + " " + .object.type')"
sha="${_ref%% *}"
objtype="${_ref##* }"
if [ -z "$sha" ] || [ "$sha" = "null" ]; then if [ -z "$sha" ] || [ "$sha" = "null" ]; then
echo "Could not resolve refs/tags/$TAG" echo "Could not resolve refs/tags/$TAG"
exit 1 exit 1
fi fi
gh api "repos/$REPO/git/refs" -f ref="refs/tags/v$TAG" -f sha="$sha" if gh api "repos/$REPO/git/ref/tags/v$TAG" >/dev/null 2>&1; then
echo "Created tag v$TAG -> $sha" echo "Tag v$TAG already exists, nothing to do."
else
gh api "repos/$REPO/git/refs" -f ref="refs/tags/v$TAG" -f sha="$sha"
echo "Created tag v$TAG -> $sha"
fi
- name: Check that the tag is signable
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
# A release published from the GitHub UI creates the tag server-side
# as a lightweight ref, which points straight at a commit and can
# never carry a signature (3.1.5 shipped that way, see issue 7273).
# The tag has to be created locally with "git tag -s" and pushed
# BEFORE the release is published, then selected on the release form.
objtype="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq .object.type)"
if [ "$objtype" != "tag" ]; then
echo "::error::refs/tags/$TAG points at a $objtype, not a tag object. It is a lightweight tag and carries no signature. Recreate it locally with 'git tag -s $TAG' and push it before publishing the release."
exit 1
fi
echo "refs/tags/$TAG is a tag object."
+4 -4
View File
@@ -233,7 +233,7 @@ acme.sh -h
#### 🔏 Verify a Release #### 🔏 Verify a Release
Release tags from `3.1.5` on are signed with the maintainer's SSH key. The Release tags from `3.1.6` on are signed with the maintainer's SSH key. The
signing happens on the maintainer's machine, so the private key is never 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 available to CI. The public half is [`allowed_signers`](allowed_signers) in
this repository. From a clone: this repository. From a clone:
@@ -243,7 +243,7 @@ git config gpg.ssh.allowedSignersFile allowed_signers
``` ```
```bash ```bash
git verify-tag 3.1.5 git verify-tag 3.1.6
``` ```
The signature covers the tag object, which pins the commit and therefore the The signature covers the tag object, which pins the commit and therefore the
@@ -251,10 +251,10 @@ 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: separate tarball checksum is needed. Build a tarball from the verified tag:
```bash ```bash
git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz git archive --format=tar.gz --prefix=acme.sh-3.1.6/ 3.1.6 > acme.sh-3.1.6.tar.gz
``` ```
> ⚠️ Tags up to `3.1.4` predate the signing key and are unsigned. > ⚠️ Tags up to `3.1.5` are unsigned.
--- ---
+208 -50
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env sh #!/usr/bin/env sh
VER=3.1.5 VER=3.1.7
PROJECT_NAME="acme.sh" PROJECT_NAME="acme.sh"
@@ -666,8 +666,15 @@ _hex_dump() {
xxd -ps -c 20 -i | sed "s/ 0x/ /g" | tr -d ",\n" | tr -s " " xxd -ps -c 20 -i | sed "s/ 0x/ /g" | tr -d ",\n" | tr -s " "
else else
_debug3 "using _ascii_hex" _debug3 "using _ascii_hex"
str=$(cat) #line by line: the cut -c in _ascii_hex works per line, and a $(cat)
_ascii_hex "$str" #capture would drop the trailing newlines (illumos without od in PATH)
while IFS= read -r _hd_line; do
_ascii_hex "$_hd_line"
printf " 0a"
done
if [ -n "$_hd_line" ]; then
_ascii_hex "$_hd_line"
fi
fi fi
} }
@@ -907,7 +914,12 @@ _json_encode() {
_j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")" _j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")"
_debug3 "_json_encode" _debug3 "_json_encode"
_debug3 "_j_str" "$_j_str" _debug3 "_j_str" "$_j_str"
echo "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n" #printf, not echo: dash's echo interprets backslash escapes and would
#collapse the \\ sequences (e.g. Telegram MarkdownV2 escapes, issue 7212)
_j_hex="$(printf "%s\n" "$_j_str" | _hex_dump | _lower_case)"
#the hex line must end with a newline: Solaris sed drops an unterminated
#last line, which emptied the whole output
printf "%s\n" "$_j_hex" | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
} }
#from: http:\/\/ to http:// #from: http:\/\/ to http://
@@ -915,7 +927,7 @@ _json_decode() {
_j_str="$(sed 's#\\/#/#g')" _j_str="$(sed 's#\\/#/#g')"
_debug3 "_json_decode" _debug3 "_json_decode"
_debug3 "_j_str" "$_j_str" _debug3 "_j_str" "$_j_str"
echo "$_j_str" printf "%s\n" "$_j_str"
} }
#extract the authorization URLs from an order response on stdin, as a #extract the authorization URLs from an order response on stdin, as a
@@ -2140,6 +2152,7 @@ _resethttp() {
__HTTP_INITIALIZED="" __HTTP_INITIALIZED=""
_ACME_CURL="" _ACME_CURL=""
_ACME_WGET="" _ACME_WGET=""
_ACME_WGET2=""
ACME_HTTP_NO_REDIRECTS="" ACME_HTTP_NO_REDIRECTS=""
} }
@@ -2190,7 +2203,15 @@ _inithttp() {
fi fi
if [ -z "$_ACME_WGET" ] && _exists "wget"; then if [ -z "$_ACME_WGET" ] && _exists "wget"; then
_ACME_WGET="wget -q" #wget2, the wget of Fedora 40 and later, prints nothing for -S under -q,
#so it runs without -q and _wget2_fix_header cleans up after it
_ACME_WGET2=""
if _contains "$(wget --version 2>&1 | _head_n 1)" "Wget2"; then
_ACME_WGET2=1
_ACME_WGET="wget"
else
_ACME_WGET="wget -q"
fi
if [ "$ACME_USE_IPV6_REQUESTS" ]; then if [ "$ACME_USE_IPV6_REQUESTS" ]; then
_ACME_WGET="$_ACME_WGET --inet6-only " _ACME_WGET="$_ACME_WGET --inet6-only "
elif [ "$ACME_USE_IPV4_REQUESTS" ]; then elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
@@ -2200,7 +2221,8 @@ _inithttp() {
_ACME_WGET="$_ACME_WGET --max-redirect 0 " _ACME_WGET="$_ACME_WGET --max-redirect 0 "
fi fi
if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
if [ "$_ACME_WGET" ] && _contains "$($_ACME_WGET --help 2>&1)" "--debug"; then #the -d demultiplexing after each request expects wget 1.x output
if [ -z "$_ACME_WGET2" ] && _contains "$($_ACME_WGET --help 2>&1)" "--debug"; then
_ACME_WGET="$_ACME_WGET -d " _ACME_WGET="$_ACME_WGET -d "
fi fi
fi fi
@@ -2220,6 +2242,30 @@ _inithttp() {
} }
#stdin: what wget2 -S wrote to stderr. Prints the response headers the way
#curl --dump-header writes them. wget2 frames every header block with lines of
#its own ("[0] Downloading ...", "# got header ...", and after the block
#"HTTP response 200 OK [url]", which callers would take for the status line),
#and prints no header block at all for a response without a body (a 204, the
#empty 200 of a revocation): then only the status line can be rebuilt, from
#the last "HTTP [ERROR ]response" line, with HTTP/1.1 assumed.
_wget2_headers() {
_w2h_in="$(cat)"
_w2h_cr="$(printf '\r')"
_w2h_blocks="$(printf "%s\n" "$_w2h_in" | sed -n "/^HTTP /d; /^HTTP\//,/^$_w2h_cr*\$/p")"
if [ "$_w2h_blocks" ]; then
printf "%s\n" "$_w2h_blocks"
else
printf "%s\n" "$_w2h_in" | sed -n 's/^HTTP ERROR response /HTTP response /; s/^HTTP response \([0-9][0-9]*\).*$/HTTP\/1.1 \1/p' | _tail_n 1
fi
}
#rewrite $HTTP_HEADER after a wget2 request, see _wget2_headers
_wget2_fix_header() {
_w2f_headers="$(_wget2_headers <"$HTTP_HEADER")"
printf "%s\n" "$_w2f_headers" >"$HTTP_HEADER"
}
# body url [needbase64] [POST|PUT|DELETE] [ContentType] # body url [needbase64] [POST|PUT|DELETE] [ContentType]
_post() { _post() {
body="$1" body="$1"
@@ -2315,7 +2361,15 @@ _post() {
response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")"
fi fi
elif [ "$httpmethod" = "HEAD" ]; then elif [ "$httpmethod" = "HEAD" ]; then
if [ "$_postContentType" ]; then if [ "$_ACME_WGET2" ]; then
#wget2 prints no headers for a HEAD response, not even with -S, but
#--save-headers writes them into the -O file
if [ "$_postContentType" ]; then
response="$($_WGET --method HEAD --save-headers -O "$HTTP_HEADER" --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" "$_post_url" 2>/dev/null)"
else
response="$($_WGET --method HEAD --save-headers -O "$HTTP_HEADER" --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" "$_post_url" 2>/dev/null)"
fi
elif [ "$_postContentType" ]; then
response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")"
else else
response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")"
@@ -2341,6 +2395,9 @@ _post() {
cat "$HTTP_HEADER" >&2 cat "$HTTP_HEADER" >&2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
fi fi
if [ "$_ACME_WGET2" ]; then
_wget2_fix_header
fi
# remove leading whitespaces from header to match curl format # remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' "$HTTP_HEADER" _sed_i 's/^ //g' "$HTTP_HEADER"
else else
@@ -2434,6 +2491,9 @@ _post_file() {
cat "$HTTP_HEADER" >&2 cat "$HTTP_HEADER" >&2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
fi fi
if [ "$_ACME_WGET2" ]; then
_wget2_fix_header
fi
# remove leading whitespaces from header to match curl format # remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' "$HTTP_HEADER" _sed_i 's/^ //g' "$HTTP_HEADER"
else else
@@ -2501,6 +2561,9 @@ _get() {
cat "$HTTP_HEADER" >&2 cat "$HTTP_HEADER" >&2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
fi fi
if [ "$_ACME_WGET2" ]; then
_wget2_fix_header
fi
# remove leading whitespaces from header to match curl format # remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' "$HTTP_HEADER" _sed_i 's/^ //g' "$HTTP_HEADER"
fi fi
@@ -2687,10 +2750,6 @@ _send_signed_request() {
_debug2 _headers "$_headers" _debug2 _headers "$_headers"
_CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" _CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)"
fi fi
if [ "$?" != "0" ]; then
_err "Cannot connect to $nonceurl to get nonce."
return 1
fi
else else
_debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE" _debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE"
fi fi
@@ -2729,11 +2788,19 @@ _send_signed_request() {
_debug3 body "$body" _debug3 body "$body"
response="$(_post "$body" "$url" "$needbase64" "POST" "$__request_conent_type")" response="$(_post "$body" "$url" "$needbase64" "POST" "$__request_conent_type")"
_post_ret="$?"
_CACHED_NONCE="" _CACHED_NONCE=""
if [ "$?" != "0" ]; then #A transport failure (curl 7, 28, 35, ...) brings no answer at all. Issue
_err "Cannot make POST request to $url" #7320: the status was tested only after the line above had reset it, so
return 1 #such a failure went on as a success, the code read from whatever
#$HTTP_HEADER still held, and the caller blamed the CA ("Finalize code was
#not 200"). Retry it the way a gateway error is retried, with a new nonce.
if [ "$_post_ret" != "0" ]; then
_sleep_post_retry_sec="$(_retry_backoff_sec "$_request_retry_times")"
_info "Cannot make POST request to $url, let's try again. Sleeping for $_sleep_post_retry_sec seconds. ($_request_retry_times/$MAX_REQUEST_RETRY_TIMES)"
_sleep "$_sleep_post_retry_sec"
continue
fi fi
responseHeaders="$(cat "$HTTP_HEADER")" responseHeaders="$(cat "$HTTP_HEADER")"
@@ -2800,6 +2867,35 @@ _sed_escape_rhs() {
sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g' sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g'
} }
#_write_conf file content
#Replace the conf file with the content.
#Redirecting straight into the conf truncates it before anything is written, so
#a failed write (e.g. no space left on device) left an empty conf and the cert
#could not be renewed any more (#7247). Write a temp file next to the conf and
#rename it over the conf only after the content is verified.
_write_conf() {
__w_conf="$1"
__w_text="$2"
__w_tmp="$__w_conf.$$.tmp"
#cp -p, so the temp file carries the mode and owner of the conf
if ! cp -p "$__w_conf" "$__w_tmp" 2>/dev/null ||
! printf -- "%s\n" "$__w_text" 2>/dev/null >"$__w_tmp" ||
[ "$(cat "$__w_tmp")" != "$__w_text" ]; then
rm -f "$__w_tmp"
return 1
fi
if [ ! -L "$__w_conf" ] && mv -f "$__w_tmp" "$__w_conf" 2>/dev/null; then
return 0
fi
#a symlink or a bind mounted file cannot be renamed over, write in place
if ! cat "$__w_tmp" 2>/dev/null >"$__w_conf"; then
#the conf may be truncated now, the temp file is the only complete copy
_err "Cannot write $__w_conf, the new content is kept in $__w_tmp"
return 1
fi
rm -f "$__w_tmp"
}
#setopt "file" "opt" "=" "value" [";"] #setopt "file" "opt" "=" "value" [";"]
_setopt() { _setopt() {
__conf="$1" __conf="$1"
@@ -2826,39 +2922,37 @@ _setopt() {
return 1 return 1
;; ;;
esac esac
if [ -n "$(_tail_c 1 <"$__conf")" ]; then if ! __text="$(cat "$__conf")"; then
echo >>"$__conf" _err "Cannot read $__conf."
return 1
fi fi
__old_text="$__text"
#build the new content first and write it once through _write_conf:
#redirecting straight into the conf truncates it before anything is
#written, so a failing sed (#2426) or a failing write (#7247) left a
#truncated conf, and a short append left a half written line
__sed_err=""
if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then
_debug3 OK _debug3 OK
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
text="$(cat "$__conf")" __text="$(printf -- "%s\n" "$__text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1
#capture first, write only on success: redirecting sed straight into the
#conf file truncates it before sed runs, so a failing sed (e.g. on an
#unescaped special character in the value) wiped the whole conf (#2426)
if __text="$(printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then
printf -- "%s\n" "$__text" >"$__conf"
else
_err "Cannot save '$__opt' to $__conf."
return 1
fi
elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
text="$(cat "$__conf")" __text="$(printf -- "%s\n" "$__text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1
if __text="$(printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then
printf -- "%s\n" "$__text" >"$__conf"
else
_err "Cannot save '$__opt' to $__conf."
return 1
fi
else else
_debug3 APP _debug3 APP
#printf, not echo: dash's builtin echo interprets backslash escapes in __text="$__text${__text:+$__nl}$__opt$__sep$__val$__end"
#the value and would corrupt it fi
printf -- "%s\n" "$__opt$__sep$__val$__end" >>"$__conf" if [ -z "$__sed_err" ] && [ "$__text" = "$__old_text" ]; then
#the value is already saved: callers re-save the same values on every
#run, and each write is a temp file plus a rename, which is slow on
#some storage (#7335)
_debug3 "Unchanged, skip writing $__conf"
return 0
fi
if [ "$__sed_err" ] || ! _write_conf "$__conf" "$__text"; then
_err "Cannot save '$__opt' to $__conf."
return 1
fi fi
_debug3 "$(grep -n "^$__opt$__sep" "$__conf")" _debug3 "$(grep -n "^$__opt$__sep" "$__conf")"
} }
@@ -2885,10 +2979,22 @@ _clear_conf() {
_c_c_f="$1" _c_c_f="$1"
_sdkey="$2" _sdkey="$2"
if [ "$_c_c_f" ]; then if [ "$_c_c_f" ]; then
_conf_data="$(cat "$_c_c_f")" _c_c_old="$(cat "$_c_c_f")"
#printf, not echo: dash's builtin echo interprets backslash escapes and #printf, not echo: dash's builtin echo interprets backslash escapes and
#would corrupt saved values that contain them on every rewrite #would corrupt saved values that contain them on every rewrite
printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f" if ! _conf_data="$(printf -- "%s\n" "$_c_c_old" | sed "/^$_sdkey *=.*$/d")"; then
_err "Cannot clear '$_sdkey' in $_c_c_f."
return 1
fi
#the key is not there, the usual case for the legacy keys cleared on
#every _savedeployconf: nothing to write (#7335)
if [ "$_conf_data" = "$_c_c_old" ]; then
return 0
fi
if ! _write_conf "$_c_c_f" "$_conf_data"; then
_err "Cannot clear '$_sdkey' in $_c_c_f."
return 1
fi
else else
_err "Config file is empty, cannot clear" _err "Config file is empty, cannot clear"
fi fi
@@ -3365,7 +3471,13 @@ __initHome() {
if [ -z "$ACCOUNT_CONF_PATH" ]; then if [ -z "$ACCOUNT_CONF_PATH" ]; then
if [ -f "$_DEFAULT_ACCOUNT_CONF_PATH" ]; then if [ -f "$_DEFAULT_ACCOUNT_CONF_PATH" ]; then
#Same as in _initpath: keep the live ACCOUNT_EMAIL across the sourcing,
#so that the -m address _process() exported is not replaced by the
#saved one. _process() calls __initHome directly, after the option
#loop, so this is the sourcing that -m used to lose to.
_ih_account_email="$ACCOUNT_EMAIL"
. "$_DEFAULT_ACCOUNT_CONF_PATH" . "$_DEFAULT_ACCOUNT_CONF_PATH"
ACCOUNT_EMAIL="$_ih_account_email"
fi fi
fi fi
@@ -3475,12 +3587,22 @@ _initpath() {
domain="$1" domain="$1"
_ilength="$2" _ilength="$2"
#Keep the live ACCOUNT_EMAIL, the one -m exported in _process() or the
#caller put in the environment. account.conf is sourced twice below (here
#and inside __initHome), and a sourced assignment would overwrite it with
#the saved address, so -m silently lost to whatever account.conf held.
#The saved address is not lost either way: _getAccountEmail() reads it
#with _readaccountconf as its last resort, after the per-CA CA_EMAIL.
_cli_account_email="$ACCOUNT_EMAIL"
__initHome __initHome
if [ -f "$ACCOUNT_CONF_PATH" ]; then if [ -f "$ACCOUNT_CONF_PATH" ]; then
. "$ACCOUNT_CONF_PATH" . "$ACCOUNT_CONF_PATH"
fi fi
ACCOUNT_EMAIL="$_cli_account_email"
if [ "$_ACME_IN_CRON" ]; then if [ "$_ACME_IN_CRON" ]; then
if [ ! "$_USER_PATH_EXPORTED" ]; then if [ ! "$_USER_PATH_EXPORTED" ]; then
_USER_PATH_EXPORTED=1 _USER_PATH_EXPORTED=1
@@ -4420,9 +4542,11 @@ _regAccount() {
_secure_debug3 _eab_kid "$_eab_kid" _secure_debug3 _eab_kid "$_eab_kid"
_secure_debug3 _eab_hmac_key "$_eab_hmac_key" _secure_debug3 _eab_hmac_key "$_eab_hmac_key"
_email="$(_getAccountEmail)" _email="$(_getAccountEmail)"
if [ "$_email" ]; then #CA_EMAIL is saved only once the CA has actually taken the contact, which
_savecaconf "CA_EMAIL" "$_email" #is when it answers 201. For an account key it already knows it answers
fi #200 and ignores the contact of the request, so saving here would record
#an address the CA never stored.
_saved_ca_email="$(_readcaconf CA_EMAIL)"
if [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then if [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then
if [ -z "$_eab_kid" ] || [ -z "$_eab_hmac_key" ]; then if [ -z "$_eab_kid" ] || [ -z "$_eab_hmac_key" ]; then
@@ -4501,8 +4625,15 @@ _regAccount() {
if [ "$code" = "" ] || [ "$code" = '201' ]; then if [ "$code" = "" ] || [ "$code" = '201' ]; then
echo "$response" >"$ACCOUNT_JSON_PATH" echo "$response" >"$ACCOUNT_JSON_PATH"
_info "Registered" _info "Registered"
if [ "$_email" ]; then
_savecaconf "CA_EMAIL" "$_email"
fi
elif [ "$code" = '409' ] || [ "$code" = '200' ]; then elif [ "$code" = '409' ] || [ "$code" = '200' ]; then
_info "Already registered" _info "Already registered"
if [ "$_email" ] && [ "$_email" != "$_saved_ca_email" ]; then
_info "The account email was not changed, the CA ignores the contact of an account it already has."
_info "Use '$PROJECT_ENTRY --update-account -m $_email' to change it."
fi
elif [ "$code" = '400' ] && _contains "$response" 'The account is not awaiting external account binding'; then elif [ "$code" = '400' ] && _contains "$response" 'The account is not awaiting external account binding'; then
_info "EAB already registered" _info "EAB already registered"
_eabAlreadyBound=1 _eabAlreadyBound=1
@@ -5590,11 +5721,9 @@ issue() {
_on_issue_err "$_post_hook" _on_issue_err "$_post_hook"
return 1 return 1
fi fi
# RFC 9773 Section 5 only defines the "alreadyReplaced" error, but real CAs # Retry without "replaces" whenever the CA rejected that field, e.g. after
# (Let's Encrypt) may also reject with a malformed error if the prior cert # switching the ACME server: the prior cert belongs to the old CA.
# was issued by a different issuer / different CA. Retry without "replaces" if [ "$_replaces_certID" ] && _isARIReplacesRejected "$code" "$response"; then
# whenever the failure mentions ARI or the replaces field.
if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "urn:ietf:params:acme:error:malformed" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then
_info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'." _info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'."
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
_err "Error creating new order." _err "Error creating new order."
@@ -7930,6 +8059,35 @@ _getARICertID() {
printf "%s.%s" "$_akiurl" "$_serurl" printf "%s.%s" "$_akiurl" "$_serurl"
} }
#httpcode response
#Returns 0 when a newOrder was rejected because of the ARI "replaces" field,
#so that the order can be retried without it.
#The status code decides first, and an empty code counts as "not rejected":
#an ACCEPTED order echoes the field back, since RFC 9773 Section 5 says that
#a server accepting a newOrder request with a "replaces" field "MUST reflect
#that field in the response", and the certID it carries is base64url, so the
#response of a SUCCESSFUL order can contain "replaces" and even "ARI".
#Matching on the message alone would then re-order without "replaces" and
#defeat ARI.
#Only the 409 "alreadyReplaced" type is mandated by RFC 9773 Section 5; the
#other checks it lists (same ACME account, shared identifier) are left to
#server policy, so the wording differs per CA: Let's Encrypt answers
#malformed when the prior cert was issued by a different issuer, ZeroSSL
#answers 401 with 'The "replaces" field does not identify a certificate that
#belongs to this ACME account'.
#https://github.com/acmesh-official/acme.sh/issues/7280
_isARIReplacesRejected() {
_ari_rej_code="$1"
_ari_rej_resp="$2"
if [ -z "$_ari_rej_code" ] || _startswith "$_ari_rej_code" "2"; then
return 1
fi
_contains "$_ari_rej_resp" "alreadyReplaced" ||
_contains "$_ari_rej_resp" "replaces" ||
_contains "$_ari_rej_resp" "ARI" ||
_contains "$_ari_rej_resp" "urn:ietf:params:acme:error:malformed"
}
#cert #cert
_get_ARI() { _get_ARI() {
_cert="$1" _cert="$1"
+4 -2
View File
@@ -4,17 +4,19 @@
# maintainer and is never available to CI, so a compromise of the build # maintainer and is never available to CI, so a compromise of the build
# pipeline cannot produce a tag that verifies against this file. # pipeline cannot produce a tag that verifies against this file.
# #
# Signing starts at 3.1.6; tags up to 3.1.5 are unsigned.
#
# Fingerprint: SHA256:M60qVafm/NUywQHXAkoQcj2v6KgkfrdSXv6mPejUUeE # Fingerprint: SHA256:M60qVafm/NUywQHXAkoQcj2v6KgkfrdSXv6mPejUUeE
# #
# To verify a release tag, from a clone of this repository: # To verify a release tag, from a clone of this repository:
# #
# git config gpg.ssh.allowedSignersFile allowed_signers # git config gpg.ssh.allowedSignersFile allowed_signers
# git verify-tag 3.1.5 # git verify-tag 3.1.6
# #
# A good signature covers the tag object, which pins the commit, which pins # 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 # the whole tree -- so verifying the tag verifies every file at that
# release. Build a tarball from the verified tag with: # 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 # git archive --format=tar.gz --prefix=acme.sh-3.1.6/ 3.1.6 > acme.sh-3.1.6.tar.gz
# #
github@neilpang.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTjI0HBJn3uhfT2DsNcFybfAZi3ADbIacMpz1BItKdB github@neilpang.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTjI0HBJn3uhfT2DsNcFybfAZi3ADbIacMpz1BItKdB
+2 -2
View File
@@ -74,9 +74,9 @@ fritzbox_deploy() {
_info "Log in to the FRITZ!Box" _info "Log in to the FRITZ!Box"
_fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')" _fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
if _exists iconv; then if _exists iconv; then
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF16LE | _digest md5 hex)" _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF-16LE | _digest md5 hex)"
elif _exists uconv; then elif _exists uconv; then
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF16LE | _digest md5 hex)" _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF-16LE | _digest md5 hex)"
else else
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | _digest md5 hex)" _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | _digest md5 hex)"
fi fi
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to an HP printer's Embedded Web Server (EWS).
#
# Docs: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#47-deploy-the-certificate-to-an-hp-printer-ews
#
# Tested on an HP Color LaserJet Pro MFP M479fdw, firmware
# CLRWTRXXXN002.2539E.00. It should work on any modern HP printer whose EWS
# exposes the "Install Certificate" flow at
# /Security/DeviceCertificates/NewCertWithPassword/Upload -- that endpoint
# takes a password-protected PKCS#12 bundle, which this hook builds from the
# issued certificate, its key and the CA chain.
#
# ```sh
# export HP_EWS_PASSWORD=mysecret
# export HP_EWS_INSECURE=1 # needed until the printer serves a trusted cert
# acme.sh --deploy -d printer.example.com --deploy-hook hp_ews
# ```
#
# Environment variables:
# HP_EWS_PASSWORD (required) EWS administrator password.
# HP_EWS_HOST (optional) IP or hostname of the printer.
# Default: the certificate's domain.
# HP_EWS_USERNAME (optional) EWS administrator user. Default: admin
# HP_EWS_PFX_PASSWORD (optional) Password for the generated PKCS#12
# bundle. A random one is used per run if
# unset; set it only if you want the
# bundle to be reproducible.
# HP_EWS_INSECURE (optional) Set to a non-empty value to skip TLS
# verification when talking to the
# printer. Needed on the first deploy,
# because the printer still serves its
# self-signed certificate at that point.
# acme.sh's global HTTPS_INSECURE is
# honoured as well.
#
# The hook authenticates with HTTP Basic auth. Printers whose EWS only offers
# Digest auth are not supported.
#
# Requires openssl, to build the PKCS#12 bundle.
#
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain pfx
hp_ews_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cpfx="$6"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
_getdeployconf HP_EWS_HOST
_getdeployconf HP_EWS_USERNAME
_getdeployconf HP_EWS_PASSWORD
_getdeployconf HP_EWS_PFX_PASSWORD
_getdeployconf HP_EWS_INSECURE
HP_EWS_HOST="${HP_EWS_HOST:-$_cdomain}"
HP_EWS_USERNAME="${HP_EWS_USERNAME:-admin}"
_debug HP_EWS_HOST "$HP_EWS_HOST"
_debug HP_EWS_USERNAME "$HP_EWS_USERNAME"
_secure_debug HP_EWS_PASSWORD "$HP_EWS_PASSWORD"
_secure_debug HP_EWS_PFX_PASSWORD "$HP_EWS_PFX_PASSWORD"
_debug HP_EWS_INSECURE "$HP_EWS_INSECURE"
if [ -z "$HP_EWS_PASSWORD" ]; then
_err "HP_EWS_PASSWORD is required. Please set the password of the printer's EWS administrator account."
return 1
fi
_savedeployconf HP_EWS_HOST "$HP_EWS_HOST"
_savedeployconf HP_EWS_USERNAME "$HP_EWS_USERNAME"
_savedeployconf HP_EWS_PASSWORD "$HP_EWS_PASSWORD" "base64"
_savedeployconf HP_EWS_PFX_PASSWORD "$HP_EWS_PFX_PASSWORD" "base64"
_savedeployconf HP_EWS_INSECURE "$HP_EWS_INSECURE"
if ! _exists "${ACME_OPENSSL_BIN:-openssl}"; then
_err "openssl is required by this deploy hook, but was not found."
return 1
fi
# The bundle password only has to survive the single upload below, so a
# fresh random one is used unless the operator asked for a fixed value.
_HP_EWS_PFXPW="$HP_EWS_PFX_PASSWORD"
if [ -z "$_HP_EWS_PFXPW" ]; then
_HP_EWS_PFXPW="$(${ACME_OPENSSL_BIN:-openssl} rand -hex 24)"
if [ -z "$_HP_EWS_PFXPW" ]; then
_err "Could not generate a random password for the PKCS#12 bundle."
return 1
fi
fi
export _HP_EWS_PFXPW
# $6 is only materialised when Le_PFXPassword is set for this domain, and
# it would be built without the CA chain, so build our own bundle instead.
_hp_ews_pfx="$(_mktemp)"
_hp_ews_body="$(_mktemp)"
if [ -z "$_hp_ews_pfx" ] || [ -z "$_hp_ews_body" ]; then
_err "Could not create the temporary files needed for the upload."
_hp_ews_cleanup
return 1
fi
_hp_ews_ret=0
if ! _hp_ews_make_pfx; then
_hp_ews_ret=1
elif ! _hp_ews_upload; then
_hp_ews_ret=1
fi
_hp_ews_cleanup
return "$_hp_ews_ret"
}
#################### Private functions below ##################################
_hp_ews_cleanup() {
[ -n "$_hp_ews_pfx" ] && rm -f "$_hp_ews_pfx"
[ -n "$_hp_ews_body" ] && rm -f "$_hp_ews_body"
unset _HP_EWS_PFXPW
# Do not leak the request header into any hook that runs after this one.
export _H1=""
return 0
}
# Bundle key, certificate and chain into $_hp_ews_pfx, protected by
# $_HP_EWS_PFXPW. The password goes through the environment rather than the
# command line so that it does not show up in ps output.
_hp_ews_make_pfx() {
_info "Building the PKCS#12 bundle"
if ! ${ACME_OPENSSL_BIN:-openssl} pkcs12 -export \
-out "$_hp_ews_pfx" \
-inkey "$_ckey" \
-in "$_ccert" \
-certfile "$_cca" \
-passout env:_HP_EWS_PFXPW; then
_err "Failed to build the PKCS#12 bundle."
return 1
fi
if [ ! -s "$_hp_ews_pfx" ]; then
_err "The PKCS#12 bundle is empty."
return 1
fi
return 0
}
# Build the multipart body in $_hp_ews_body and POST it to the printer.
_hp_ews_upload() {
_hp_ews_url="https://${HP_EWS_HOST}/Security/DeviceCertificates/NewCertWithPassword/Upload"
_hp_ews_boundary="----------------------------acmesh$(${ACME_OPENSSL_BIN:-openssl} rand -hex 16)"
{
printf -- "--%s\r\n" "$_hp_ews_boundary"
printf "Content-Disposition: form-data; name=\"certificate\"; filename=\"%s.pfx\"\r\n" "$_cdomain"
printf "Content-Type: application/x-pkcs12\r\n\r\n"
cat "$_hp_ews_pfx"
printf "\r\n"
printf -- "--%s\r\n" "$_hp_ews_boundary"
printf "Content-Disposition: form-data; name=\"password\"\r\n\r\n%s\r\n" "$_HP_EWS_PFXPW"
printf -- "--%s--\r\n" "$_hp_ews_boundary"
} >"$_hp_ews_body"
_H1="Authorization: Basic $(printf "%s:%s" "$HP_EWS_USERNAME" "$HP_EWS_PASSWORD" | _base64)"
export _H1
if [ "$HP_EWS_INSECURE" ]; then
export HTTPS_INSECURE=1
fi
_info "Uploading the certificate to $_hp_ews_url"
if ! _post_file "$_hp_ews_body" "$_hp_ews_url" "" "POST" "multipart/form-data; boundary=$_hp_ews_boundary" >/dev/null; then
if [ -z "$HTTPS_INSECURE" ]; then
_err "If the printer still serves its self-signed certificate, set HP_EWS_INSECURE=1 and try again."
fi
return 1
fi
# The EWS answers 201 Created on a successful install.
_hp_ews_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug _hp_ews_code "$_hp_ews_code"
if [ "$_hp_ews_code" != "201" ] && [ "$_hp_ews_code" != "200" ]; then
_err "The printer rejected the certificate (HTTP $_hp_ews_code)."
if [ "$_hp_ews_code" = "401" ] || [ "$_hp_ews_code" = "403" ]; then
_err "Check HP_EWS_USERNAME and HP_EWS_PASSWORD. Only HTTP Basic authentication is"
_err "supported; printers that only offer Digest cannot be used with this hook."
fi
return 1
fi
_info "Certificate installed (HTTP $_hp_ews_code). The printer restarts its web server shortly."
return 0
}
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env sh
# Deploy hook to deploy certificate and key to nginx proxy manager/NPMPlus using
# REST API. Separately uploading the intermediate for nginx proxy manager is
# not necessary as the "fullchain" is always used for the certificate.
#
# Environment variables:
#
# DEPLOY_NPM_HOST - example.com, 192.168.1.1, etc. (REQUIRED)
# Host/IP address of NPM admin panel
# DEPLOY_NPM_PORT - 80, 443, 8443, etc. (REQUIRED)
# Port number of NPM admin panel
# DEPLOY_NPM_PROTOCOL - http/https (REQUIRED)
# Protocol to connect to NPM admin panel. https is
# STRONGLY recommended
# DEPLOY_NPM_CERTNUM - 1, 2, 3, etc. (OPTIONAL)
# the ID number of the custom certificate to overwrite.
# Find it in the NPM admin panel under certificates.
# Must already exist.
# DEPLOY_NPM_USER - admin, npmuser, etc. (REQUIRED)
# Authorized user for NPM. Must have permission
# to manage certificates.
# DEPLOY_NPM_PASSWORD - mysecurepassword, mysecretpass, etc. (REQUIRED)
# Authorized user's password for NPM 2FA is not supported
# since neither nginx proxy manager nor NPMplus support
# permanent API tokens. Either disable 2FA for the
# administrator account, or (better option) create a new user
# with 2FA disabled, and grant this user permissions for
# managing certificates.
# DEPLOY_NPM_CERTNAME My Custom Certificate, Cool Certificate Name, etc. (OPTIONAL)
# Name of custom certificate to create. This is only used
# to create a certificate the first time. It is IGNORED
# once DEPLOY_NPM_CERTNUM is set. If neither certificate
# ID number and name are provided, a default name of
# "acme.sh custom certificate" with a timestamp suffix will be
# used
#
######## Public functions #####################
#domain keyfile certfile cafile fullchain
nginx_proxy_manager_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cpfx="$6"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
_getdeployconf DEPLOY_NPM_HOST
_getdeployconf DEPLOY_NPM_PORT
_getdeployconf DEPLOY_NPM_PROTOCOL
_getdeployconf DEPLOY_NPM_CERTNUM
_getdeployconf DEPLOY_NPM_USER
_getdeployconf DEPLOY_NPM_PASSWORD
_getdeployconf DEPLOY_NPM_CERTNAME
if [ -z "$DEPLOY_NPM_HOST" ]; then
_err "DEPLOY_NPM_HOST must be defined"
return 1
fi
if [ -z "$DEPLOY_NPM_PORT" ]; then
_err "DEPLOY_NPM_PORT must be defined"
return 1
fi
if [ -z "$DEPLOY_NPM_CERTNUM" ]; then
_info "DEPLOY_NPM_CERTNUM not provided, a new certificate will be created."
if [ -z "$DEPLOY_NPM_CERTNAME" ]; then
DEPLOY_NPM_CERTNAME="acme.sh custom certificate - $(_utc_date | tr ' ' '_' | tr -d -- ':')"
_info "DEPLOY_NPM_CERTNAME not set. Using default:"
_info DEPLOY_NPM_CERTNAME "$DEPLOY_NPM_CERTNAME"
fi
fi
if [ "$DEPLOY_NPM_PROTOCOL" != "https" ] && [ "$DEPLOY_NPM_PROTOCOL" != "http" ]; then
_err "DEPLOY_NPM_PROTOCOL must be either http or https"
return 1
fi
if [ -z "$DEPLOY_NPM_USER" ]; then
_err "DEPLOY_NPM_USER must be defined"
return 1
fi
if [ -z "$DEPLOY_NPM_PASSWORD" ]; then
_err "DEPLOY_NPM_PASSWORD must be defined"
return 1
fi
# extract and return response code
_npm_response_code() {
_npm_code="$(_egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n")"
printf '%s\n' "$_npm_code" | _egrep_o "^[0-9][0-9]*"
}
# strip backslashes, encode for JSON, and remove newlines
_npm_payload_format() {
_npm_payload_out="$(printf "%s\n" "$1" | _json_encode)"
printf '%s' "${_npm_payload_out%\\n}"
}
_debug DEPLOY_NPM_USER "$DEPLOY_NPM_USER"
_secure_debug DEPLOY_NPM_PASSWORD "$DEPLOY_NPM_PASSWORD"
_npm_user_json="$(_npm_payload_format "$DEPLOY_NPM_USER")"
_npm_password_json="$(_npm_payload_format "$DEPLOY_NPM_PASSWORD")"
_debug _npm_user_json "$_npm_user_json"
_secure_debug _npm_password_json "$_npm_password_json"
_info "Authenticating and fetching temporary API token"
_npm_token_raw="$(_post '{"identity":"'"$_npm_user_json"'","secret":"'"$_npm_password_json"'"}' "${DEPLOY_NPM_PROTOCOL}://${DEPLOY_NPM_HOST}:${DEPLOY_NPM_PORT}/api/tokens" "" "POST" "application/json")"
_secure_debug _npm_token_raw "$_npm_token_raw"
_npm_requires2fa="$(printf '%s' "$_npm_token_raw" | _egrep_o '"requires_2fa" *: *[^,}]*' | cut -d : -f 2 | tr -d ' ')"
_npm_requiresTotp="$(printf '%s' "$_npm_token_raw" | _egrep_o '"requiresTotp" *: *[^,}]*' | cut -d : -f 2 | tr -d ' ')"
if [ "$_npm_requires2fa" = "true" ] || [ "$_npm_requiresTotp" = "true" ]; then
_err "2FA/MFA is enabled, deployment is not supported. Disable it or create a user with 2FA/MFA disabled and update your configuration."
return 1
fi
_npm_token_code="$(_npm_response_code)"
_debug _npm_token_code "$_npm_token_code"
if [ "$_npm_token_code" != "200" ]; then
_err "Failed to retrieve a token, server response code: $_npm_token_code"
return 1
fi
# first look for a token in JSON
_npm_type="Nginx Proxy Manager API"
_npm_token="$(printf '%s' "$_npm_token_raw" | _egrep_o '"token" *: *"[^"]*"' | cut -d '"' -f 4)"
if [ "$_npm_token" = "" ]; then
_debug "Token not found in JSON (nginx proxy manager API), checking HTTP header"
# look for a token in the header
_npm_type="NPMplus API"
_npm_token="$(grep <"$HTTP_HEADER" -i "^Set-Cookie: *__Host-Http-token=" | _tail_n 1 | _egrep_o "__Host-Http-token=[^;]*" | _head_n 1 | cut -d'=' -f2-)"
if [ "$_npm_token" = "" ]; then
_err "Token not found in HTTP header (NPMplus API). Unable to obtain a token."
return 1
fi
fi
_info "Detected API type: $_npm_type"
_secure_debug _npm_token "$_npm_token"
_info "API token retrieved."
if [ "$_npm_type" = "Nginx Proxy Manager API" ]; then
_H1="Authorization: Bearer $_npm_token"
elif [ "$_npm_type" = "NPMplus API" ]; then
_H1="Cookie: __Host-Http-token=$_npm_token"
fi
export _H1
_secure_debug _H1 "$_H1"
_npm_nl="\0015\0012"
# Create new certificate if a certificate ID number was not provided
if [ -z "$DEPLOY_NPM_CERTNUM" ]; then
_npm_certname_json="$(_npm_payload_format "$DEPLOY_NPM_CERTNAME")"
_npm_create_result="$(_post '{"provider":"other","nice_name":"'"$_npm_certname_json"'"}' "$DEPLOY_NPM_PROTOCOL://$DEPLOY_NPM_HOST:$DEPLOY_NPM_PORT/api/nginx/certificates" "" "POST" "application/json")"
_debug _npm_create_result "$_npm_create_result"
_npm_create_code="$(_npm_response_code)"
if [ "$_npm_create_code" != "201" ]; then
_err "Failed to create new certificate, server response code: $_npm_create_code"
unset _H1
return 1
fi
DEPLOY_NPM_CERTNUM="$(printf '%s' "$_npm_create_result" | _egrep_o '"id" *: *[^,}]*' | cut -d : -f 2 | tr -d ' "')"
_info "Created certificate ID number $DEPLOY_NPM_CERTNUM"
fi
_info "Uploading certificates into entry $DEPLOY_NPM_CERTNUM"
_npm_form_boundary="--------------------------$(_utc_date | tr -d -- '-: ')"
_npm_post_payload="--$_npm_form_boundary${_npm_nl}Content-Disposition: form-data; name=\"certificate_key\"; filename=\"$(basename "$_ckey")\"${_npm_nl}Content-Type: application/octet-stream${_npm_nl}${_npm_nl}$(cat "$_ckey")\0012"
_npm_post_payload="$_npm_post_payload${_npm_nl}--$_npm_form_boundary${_npm_nl}Content-Disposition: form-data; name=\"certificate\"; filename=\"$(basename "$_cfullchain")\"${_npm_nl}Content-Type: application/octet-stream${_npm_nl}${_npm_nl}$(cat "$_cfullchain")\0012"
_npm_post_payload="$_npm_post_payload${_npm_nl}--$_npm_form_boundary--${_npm_nl}"
_npm_post_payload="$(printf "%b_" "$_npm_post_payload")"
_npm_post_payload="${_npm_post_payload%_}"
_secure_debug _npm_post_payload "$_npm_post_payload"
_npm_upload_result="$(_post "$_npm_post_payload" "${DEPLOY_NPM_PROTOCOL}://${DEPLOY_NPM_HOST}:${DEPLOY_NPM_PORT}/api/nginx/certificates/${DEPLOY_NPM_CERTNUM}/upload" "" "POST" "multipart/form-data; boundary=${_npm_form_boundary}")"
unset _H1
_secure_debug _npm_upload_result "$_npm_upload_result"
_npm_upload_code="$(_npm_response_code)"
_debug _npm_upload_code "$_npm_upload_code"
case "$_npm_upload_code" in
"404")
_err "Failed to upload certificates, server response code: $_npm_upload_code"
_err "Check that certificate number $DEPLOY_NPM_CERTNUM exists."
return 1
;;
"200")
_info "Certificate uploaded successfully"
;;
*)
_err "Failed to upload certificates, server response code: $_npm_upload_code"
return 1
;;
esac
_info "$(__green "Deployed successfully")"
_savedeployconf DEPLOY_NPM_HOST "$DEPLOY_NPM_HOST"
_savedeployconf DEPLOY_NPM_PORT "$DEPLOY_NPM_PORT"
_savedeployconf DEPLOY_NPM_PROTOCOL "$DEPLOY_NPM_PROTOCOL"
_savedeployconf DEPLOY_NPM_CERTNUM "$DEPLOY_NPM_CERTNUM"
_savedeployconf DEPLOY_NPM_USER "$DEPLOY_NPM_USER"
_savedeployconf DEPLOY_NPM_PASSWORD "$DEPLOY_NPM_PASSWORD" "base64"
return 0
}
+492
View File
@@ -0,0 +1,492 @@
#!/usr/bin/env sh
# Script to deploy certificates to compatible BMCs (baseboard management
# controllers) using the [Redfish API](https://www.dmtf.org/standards/redfish).
#
# Environment Variables:
#
# ```sh
# DEPLOY_REDFISH_HOST="ipmi.example.com" # Required
# DEPLOY_REDFISH_USERNAME="Administrator" # Required
# DEPLOY_REDFISH_PASSWORD="superuser" # Required
# DEPLOY_REDFISH_USE_BASIC_AUTH=0 # Optional (set to "1" to enable)
# DEPLOY_REDFISH_RESTART=0 # Optional (set to "1" to enable)
# DEPLOY_REDFISH_MANAGER= # Optional (e.g. "/redfish/v1/Managers/Self")
# DEPLOY_REDFISH_CERTIFICATE= # Optional (e.g. "/redfish/v1/CertificateService/Certificates/1")
# ```
#
# Compatibility:
#
# Compatible with any IPMI/BMC solution supporting Redfish v1.6.1 or newer (or
# Redfish Schema Bundle 2018.3), as defined in the following documents:
#
# <https://www.dmtf.org/sites/default/files/standards/documents/DSP0266_1.6.1.pdf>
# <https://www.dmtf.org/sites/default/files/standards/documents/DSP2046_2018.3.pdf>
# <https://www.dmtf.org/sites/default/files/standards/documents/DSP2059_1.0.0.pdf>
#
# This script is confirmed to work on the following systems:
#
# | IPMI Solution | BMC Chipset | Firmware Family | Redfish Version |
# |------------------|----------------|------------------|-----------------|
# | ASUS ASMB12-iKVM | ASPEED AST2600 | AMI MegaRAC SP-X | 1.15.1 |
#
# However, this should also work with HPE iLO 5 v2.42 (or newer), Dell iDRAC9
# v5.0 (or newer), or any ASUS/Lenovo/Supermicro IPMI solution based on AMI
# MegaRAC SP-X v12.x (or newer).
#domain keyfile certfile cafile fullchain
redfish_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 DEPLOY_REDFISH_HOST
_getdeployconf DEPLOY_REDFISH_USERNAME
_getdeployconf DEPLOY_REDFISH_PASSWORD
_getdeployconf DEPLOY_REDFISH_USE_BASIC_AUTH
_getdeployconf DEPLOY_REDFISH_RESTART
_getdeployconf DEPLOY_REDFISH_MANAGER
_getdeployconf DEPLOY_REDFISH_CERTIFICATE
if [ -z "${DEPLOY_REDFISH_HOST}" ]; then
_err 'DEPLOY_REDFISH_HOST must be set. Please specify the IP or domain name of the Redfish server.'
return 1
fi
if [ -z "${DEPLOY_REDFISH_USERNAME}" ] || [ -z "${DEPLOY_REDFISH_PASSWORD}" ]; then
_err 'DEPLOY_REDFISH_USERNAME and DEPLOY_REDFISH_PASSWORD must be set.'
return 1
fi
DEPLOY_REDFISH_USE_BASIC_AUTH="${DEPLOY_REDFISH_USE_BASIC_AUTH:-0}"
DEPLOY_REDFISH_RESTART="${DEPLOY_REDFISH_RESTART:-0}"
_debug DEPLOY_REDFISH_HOST "${DEPLOY_REDFISH_HOST}"
_debug DEPLOY_REDFISH_USERNAME "${DEPLOY_REDFISH_USERNAME}"
_secure_debug DEPLOY_REDFISH_PASSWORD "${DEPLOY_REDFISH_PASSWORD}"
_debug DEPLOY_REDFISH_USE_BASIC_AUTH "${DEPLOY_REDFISH_USE_BASIC_AUTH}"
_redfish_log_in "$DEPLOY_REDFISH_USERNAME" "$DEPLOY_REDFISH_PASSWORD" || return 1
# 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.
_savedeployconf DEPLOY_REDFISH_HOST "${DEPLOY_REDFISH_HOST}"
_savedeployconf DEPLOY_REDFISH_USERNAME "${DEPLOY_REDFISH_USERNAME}"
_savedeployconf DEPLOY_REDFISH_PASSWORD "${DEPLOY_REDFISH_PASSWORD}" 'base64'
_savedeployconf DEPLOY_REDFISH_USE_BASIC_AUTH "${DEPLOY_REDFISH_USE_BASIC_AUTH}"
if ! _redfish_get_certificate_service_endpoint; then
_err "Redfish server ${DEPLOY_REDFISH_HOST} doesn't support certificate management API."
return 1
fi
_debug DEPLOY_REDFISH_RESTART "${DEPLOY_REDFISH_RESTART}"
_debug DEPLOY_REDFISH_MANAGER "${DEPLOY_REDFISH_MANAGER}"
_debug DEPLOY_REDFISH_CERTIFICATE "${DEPLOY_REDFISH_CERTIFICATE}"
if [ -n "${DEPLOY_REDFISH_MANAGER}" ]; then
_manager_endpoint="${DEPLOY_REDFISH_MANAGER}"
else
_info 'Identifying REST endpoint of primary Redfish manager.'
_redfish_get_primary_manager_endpoint || return 1
_savedeployconf DEPLOY_REDFISH_MANAGER "${_manager_endpoint}"
fi
if [ -n "${DEPLOY_REDFISH_CERTIFICATE}" ]; then
_certificate_endpoint="${DEPLOY_REDFISH_CERTIFICATE}"
else
_info "Identifying REST endpoint of primary TLS certificate on: ${_manager_endpoint}"
_redfish_get_manager_certificate_endpoint "${_manager_endpoint}" || return 1
fi
if [ -f "${_ckey}" ] && [ -f "${_cfullchain}" ]; then
_info "Uploading private key and full certificate chain to: ${_certificate_endpoint}"
if ! _redfish_supports_full_certificate_chains; then
_err 'Redfish server does not support uploading full certificate chains!'
_err "Please call 'GenerateCSR' API on server first, pass the CSR to 'acme.sh --sign-csr --csr <key.pem>', and try again."
_err 'For more information: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#generate-csr-on-server-optional'
return 1
elif ! _redfish_supports_private_key "${_ckey}" "${_certificate_service_endpoint}"; then
_err "Redfish server does not support this type of private key."
return 1
fi
_ckey_pkcs8="$("${ACME_OPENSSL_BIN:-openssl}" pkcs8 -topk8 -nocrypt -in "${_ckey}")"
# shellcheck disable=SC2181
if [ "$?" != '0' ] || [ -z "${_ckey_pkcs8}" ]; then
_err 'Failed to convert private key to PKCS#8 format!'
return 1
fi
_certificate_str="$(echo "${_ckey_pkcs8}" | cat - "${_cfullchain}" | _json_encode)"
_certificate_type='PEMchain'
else
_info "Private key and full certificate chain not available; we must be using a server-generated CSR."
_info "Uploading only leaf certificate to: ${_certificate_endpoint}"
_certificate_str="$(_json_encode <"${_ccert}")"
_certificate_type='PEM'
fi
_body="$(printf '{"CertificateString":"%s","CertificateType":"%s","CertificateUri":{"@odata.id":"%s"}}' "${_certificate_str}" "${_certificate_type}" "${_certificate_endpoint}")"
_redfish_rest POST "${_certificate_service_endpoint}/Actions/CertificateService.ReplaceCertificate" "${_body}" || return 1
_code="$(_redfish_response_code)"
if ! _startswith "${_code}" '2'; then
_err "Failed to update Redfish server TLS certificate! (HTTP ${_code})"
_err "Response: ${_response}"
return 1
fi
_info 'Successfully updated Redfish server TLS certificate!'
_savedeployconf DEPLOY_REDFISH_CERTIFICATE "${_certificate_endpoint}"
if [ "${DEPLOY_REDFISH_RESTART}" = '1' ]; then
_info 'Attempting to restart BMC gracefully.'
_redfish_attempt_graceful_restart "${_manager_endpoint}" || return 0
_info 'Successfully sent graceful restart command to BMC. After restarting, the new TLS certificate will be active.'
else
_info 'Please wait up to 20 seconds to take effect, or restart the BMC manually.'
fi
_savedeployconf DEPLOY_REDFISH_RESTART "${DEPLOY_REDFISH_RESTART}"
return 0
}
_redfish_rest() {
_method="$1"
_endpoint="$2"
_body="${3:-}"
export _H1='OData-Version: 4.0'
if [ "${_method}" = 'GET' ]; then
_response="$(_get "https://${DEPLOY_REDFISH_HOST}${_endpoint}")"
else
_response="$(_post "${_body}" "https://${DEPLOY_REDFISH_HOST}${_endpoint}" '' "${_method}" "${_body:+application/json}")"
fi
_ret="$?"
_response="$(echo "${_response}" | _normalizeJson)"
if [ "${_ret}" != '0' ]; then
_err "Error while calling ${_method} ${_endpoint}. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot."
fi
return "${_ret}"
}
_redfish_response_code() {
_egrep_o <"${HTTP_HEADER}" '^HTTP[^ ]* [0-9]+' | _tail_n 1 | tr -d '\r\n' | cut -d ' ' -f 2
}
_redfish_get_odata_id() {
_regex='s/{'
for key in $(echo "$1" | tr '.' '\n'); do
_regex="${_regex}.*\"${key}\":{"
done
sed -n "${_regex}[^}]*\"@odata\\.id\":[ ]*\"\\([^\"]*\\)\".*/\\1/p"
}
_redfish_get_odata_members() {
sed -n 's/.*"Members":[ ]*\[\([^]]*\)\].*/\1/p' | sed 's/,/\n/g' | sed -n 's/.*{"@odata\.id":"\([^"]*\)"}.*/\1/p'
}
_redfish_get_odata_count() {
sed -n 's/.*"Members@odata\.count":[ ]*\([0-9][0-9]*\).*/\1/p'
}
_redfish_log_in() {
_username="$1"
_password="$2"
_redfish_rest GET '/redfish/v1/' || return 1
_info "Authenticating with Redfish API: ${DEPLOY_REDFISH_HOST}"
if [ "${DEPLOY_REDFISH_USE_BASIC_AUTH}" = '1' ]; then
_access_token="$(printf '%s:%s' "${_username}" "${_password}" | _base64)"
export _H2="Authorization: Basic ${_access_token}"
# Verify credentials are valid
_account_service_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'AccountService')"
_redfish_rest GET "${_account_service_endpoint}" || return 1
_code="$(_redfish_response_code)"
if [ "${_code}" != '200' ]; then
_err "Redfish authentication failed (HTTP ${_code})"
_err "Response: ${_response}"
return 1
fi
else
# Create new session
_sessions_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'Links.Sessions')"
_username="$(printf '%s\n' "${_username}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
_password="$(printf '%s\n' "${_password}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
_body="$(printf '{"UserName":"%s","Password":"%s"}' "${_username}" "${_password}")"
_redfish_rest POST "${_sessions_endpoint}" "${_body}" || return 1
_code="$(_redfish_response_code)"
# Verify authentication succeeded
if [ "${_code}" != '201' ]; then
_err "Redfish authentication failed (HTTP ${_code})"
_err "Response: ${_response}"
if [ "${_code}" = '401' ] && grep -qi '^WWW-Authenticate: Basic.*$' "${HTTP_HEADER}"; then
_err 'NOTE: Server supports Basic auth scheme. Consider setting DEPLOY_REDFISH_USE_BASIC_AUTH=1 and try again.'
fi
return 1
fi
_session="$(echo "${_response}" | _redfish_get_odata_id '')"
_auth_token="$(grep -i '^X-Auth-Token: .*$' "${HTTP_HEADER}" | _tail_n 1 | tr -d ' \r\n' | cut -d ':' -f 2)"
export _H2="X-Auth-Token: ${_auth_token}"
fi
trap '_redfish_log_out "${_session:-}"' EXIT INT
}
_redfish_log_out() {
_session_endpoint="${1:-}"
_info "Logging out of Redfish session on: ${DEPLOY_REDFISH_HOST}"
if [ -n "${_session_endpoint}" ]; then
_redfish_rest DELETE "${_session_endpoint}" || return 1
_code="$(_redfish_response_code)"
if ! _startswith "${_code}" '2'; then
_err "Failed to log out of Redfish server (HTTP ${_code})."
_err "Response: ${_response}"
fi
else
_info 'Using basic HTTP auth, no need to log out of Redfish API.'
fi
export _H1=
export _H2=
}
_redfish_get_certificate_service_endpoint() {
_redfish_rest GET '/redfish/v1/' || return 1
_certificate_service_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'CertificateService')"
[ -n "${_certificate_service_endpoint}" ]
}
_redfish_supports_full_certificate_chains() {
_redfish_rest GET "/redfish/v1/\$metadata" || return 1
_newest_certificate_schema_version="$(echo "${_response}" |
_egrep_o 'Namespace="Certificate\.v[_0-9][_0-9]*"' |
sed -n 's/.*v\([_0-9][_0-9]*\)".*/\1/p' |
sort -n -t _ -k1,1 -k2,2 -k3,3 | _tail_n 1 | tr '_' '.')"
_debug _newest_certificate_schema_version "${_newest_certificate_schema_version}"
_schema_major=${_newest_certificate_schema_version%%.*}
_schema_patch_minor=${_newest_certificate_schema_version#*.}
_schema_minor=${_schema_patch_minor%%.*}
_schema_patch=${_schema_patch_minor#*.}
for var in _schema_major _schema_minor _schema_patch; do
_debug "$var" "$(eval echo "\$$var")"
case "$(eval echo "\$$var")" in
'' | *[!0-9]*) return 1 ;;
*) continue ;;
esac
done
[ "${_schema_major}" -gt '1' ] || { [ "${_schema_major}" -ge '1' ] && [ "${_schema_minor}" -ge '4' ]; }
}
_redfish_get_action() {
unset _action _action_info
_redfish_rest GET "$1" || return 1
_action="$(echo "${_response}" | sed -n "s/{.*\"Actions\":{.*\"#$2\":\({[^}]*}\).*/\1/p")"
_action_info_endpoint="$(echo "${_action}" | sed -n 's/.*"@Redfish\.ActionInfo":"\([^"]*\)".*/\1/p')"
if [ -n "${_action_info_endpoint}" ]; then
_redfish_rest GET "${_action_info_endpoint}" || return 1
_code="$(_redfish_response_code)"
if [ "${_code}" = '200' ]; then
_action_info="${_response}"
fi
fi
}
_redfish_get_action_allowable_values() {
_parameter="$1"
_allowable_values="$(echo "${_action}" |
sed -n "s/.*\"${_parameter}@Redfish\.AllowableValues\":[ ]*\[\([^]]*\)\].*/\1/p" |
tr -d '"')"
if [ -n "${_allowable_values}" ]; then
_debug2 "Found allowable values for '${_parameter}' in '@Redfish.Action': [${_allowable_values}]"
echo "${_allowable_values}"
elif [ -n "${_action_info}" ]; then
_parameter_info="$(echo "${_action_info}" |
sed -n 's/.*"Parameters":[ ]*\[\(\({[^}]*},\{0,1\}\)*\)\].*/\1/p' | _egrep_o '\{[^\}]{1,}\}' |
grep "\"Name\":\"${_parameter}\"")"
if [ -n "${_parameter_info}" ]; then
_debug2 "Found allowable values for '${_parameter}' in @Redfish.ActionInfo: ${_parameter_info}"
_data_type="$(echo "${_parameter_info}" | sed -n 's/.*"DataType":[ ]*"\([^"]*\)".*/\1/p')"
case "${_data_type}" in
String)
echo "${_parameter_info}" | sed -n 's/.*"AllowableValues":[ ]*\[\([^]]*\)\].*/\1/p' | tr -d '"'
;;
Number)
_min_value="$(echo "${_parameter_info}" | sed -n 's/.*"MinimumValue":[ ]*\([0-9]\{1,\}\).*/\1/p')"
_max_value="$(echo "${_parameter_info}" | sed -n 's/.*"MaximumValue":[ ]*\([0-9]\{1,\}\).*/\1/p')"
echo "${_min_value},${_max_value}"
;;
esac
fi
fi
}
_redfish_supports_private_key() {
_private_key="$1"
_certificate_service_endpoint="$2"
if _isRSA "${_private_key}"; then
_key_algo='RSA'
elif _isEcc "${_private_key}"; then
_key_algo='ECDSA'
else
_err 'Private key uses unknown cryptographic algorithm!'
return 1
fi
_redfish_get_action "${_certificate_service_endpoint}" 'CertificateService.GenerateCSR' || return 1
_allowed_key_algos="$(_redfish_get_action_allowable_values 'KeyPairAlgorithm')"
_allowed_key_bit_lengths="$(_redfish_get_action_allowable_values 'KeyBitLength')"
_allowed_key_curve_ids="$(_redfish_get_action_allowable_values 'KeyCurveId')"
if [ -z "${_allowed_key_algos}" ]; then
_contains "${_response}" '"KeyBitLength"' && _allowed_key_algos='TPM_ALG_RSA'
_contains "${_response}" '"KeyCurveId"' && _allowed_key_algos="${_allowed_key_algos},TPM_ALG_ECDSA"
_allowed_key_algos="${_allowed_key_algos#,}"
if [ -z "${_allowed_key_algos}" ]; then
_debug 'Failed to determine server crypto requirements, proceeding blindly.'
return 0
fi
_debug "GenerateCSR does not specify allowed KeyPairAlgorithm(s), so assuming ${_allowed_key_algos}."
fi
_debug _key_algo "${_key_algo}"
_debug _allowed_key_algos "${_allowed_key_algos}"
case "${_key_algo}:${_allowed_key_algos}" in
RSA:*RSA*)
_min_key_length="$(echo "${_allowed_key_bit_lengths}" | cut -d ',' -f 1)"
_max_key_length="${_allowed_key_bit_lengths##*,}"
_debug _min_key_length "${_min_key_length}"
_debug _max_key_length "${_max_key_length}"
if [ -z "${_min_key_length}" ] && [ -z "${_max_key_length}" ]; then
_debug "Server supports RSA private keys but doesn't specify minimum/maximum bit length, proceeding blindly."
return 0
fi
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
if [ "${Le_Keylength}" -lt "${_min_key_length:-${Le_Keylength}}" ] || [ "${Le_Keylength}" -gt "${_max_key_length:-${Le_Keylength}}" ]; then
_err "Unsupported RSA private key length ${Le_Keylength}!"
_err "Please re-run acme.sh with --keylength set to a value between ${_min_key_length} and ${_max_key_length}."
return 1
fi
;;
ECDSA:*ECDSA*)
_debug _allowed_key_curve_ids "${_allowed_key_curve_ids}"
if [ -z "${_allowed_key_curve_ids}" ]; then
_debug "Server supports ECDSA private keys but doesn't specify which curves, proceeding blindly."
return 0
fi
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
if ! _contains "${_allowed_key_curve_ids}" "TPM_ECC_NIST_P${Le_Keylength#ec-}"; then
_err "Unsupported ECDSA private key type! Server supports only: ${_allowed_key_curve_ids}"
return 1
fi
;;
*)
_err "Unsupported key pair algorithm ${_key_algo}! Server supports only: ${_allowed_key_algos}"
return 1
;;
esac
}
_redfish_get_primary_manager_endpoint() {
_redfish_rest GET '/redfish/v1/' || return 1
_managers_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'Managers')"
_redfish_rest GET "${_managers_endpoint}" || return 1
_num_managers="$(echo "${_response}" | _redfish_get_odata_count)"
if [ "${_num_managers}" != '1' ]; then
_all_managers="$(echo "${_response}" | _redfish_get_odata_members | tr '\n' ',')"
_err "Multiple Redfish managers identified (${_all_managers}), but expected exactly one."
_err 'Please specify the correct manager in DEPLOY_REDFISH_MANAGER.'
return 1
fi
_manager_endpoint="$(echo "${_response}" | _redfish_get_odata_members | _head_n 1)"
[ -n "${_manager_endpoint}" ]
}
_redfish_get_manager_certificate_endpoint() {
_manager_endpoint="$1"
_redfish_rest GET "${_manager_endpoint}" || return 1
_protocol_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'NetworkProtocol')"
_redfish_rest GET "${_protocol_endpoint}/HTTPS/Certificates" || return 1
_num_certificates="$(echo "${_response}" | _redfish_get_odata_count)"
if [ -z "${_num_certificates}" ]; then
_certificate_endpoint="${_protocol_endpoint}/HTTPS/Certificates/1"
return 0
elif [ "${_num_certificates}" != '1' ]; then
_all_certificates="$(echo "${_response}" | _redfish_get_odata_members | tr '\n' ',')"
_err "Multiple web service HTTPS certificates identified (${_all_certificates}), but expected exactly one."
_err "Please specify which certificate should be replaced in DEPLOY_REDFISH_CERTIFICATE."
return 1
fi
_certificate_endpoint="$(echo "${_response}" | _redfish_get_odata_members | _head_n 1)"
[ -n "${_certificate_endpoint}" ]
}
_redfish_attempt_graceful_restart() {
_manager_endpoint="$1"
_redfish_get_action "${_manager_endpoint}" 'Manager.Reset' || return 1
_allowed_reset_types="$(_redfish_get_action_allowable_values 'ResetType')"
if ! _contains "${_allowed_reset_types}" 'GracefulRestart'; then
_err "BMC doesn't support graceful restarts. Please wait up to 20 seconds to take effect, or restart the BMC manually."
return 1
fi
_redfish_rest POST "${_manager_endpoint}/Actions/Manager.Reset" '{"ResetType":"GracefulRestart"}' || return 1
_code="$(_redfish_response_code)"
if ! _startswith "${_code}" '2'; then
_err "Failed to gracefully restart BMC! (HTTP ${_code})"
_err "Response: ${_response}"
return 1
fi
}
+28 -6
View File
@@ -4,18 +4,29 @@
# Deploy the HTTPS server certificate to a Shelly device on the local network. # Deploy the HTTPS server certificate to a Shelly device on the local network.
# #
# ```sh # ```sh
# export SHELLY_HOST=192.168.1.100 # export SHELLY_HOST=shelly.example.com
# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device # export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
# export SHELLY_SCHEME=https # only if devices factory shipped with
# # firmware 2.0.0 or already has https
# # enabled
# acme.sh --deploy -d shelly.example.com --deploy-hook shelly # acme.sh --deploy -d shelly.example.com --deploy-hook shelly
# ``` # ```
# #
# If `SHELLY_SCHEME=https` is used, the first deploy is likely to require
# the additional `--insecure` parameter due to factory signed certificates.
# After the initial deployment make sure to retest deployment without
# `--insecure` simply using `acme.sh --deploy -d shelly.example.com`
# to ensure follow up deployment will work.
#
# Environment variables: # Environment variables:
# SHELLY_HOST (required) IP or hostname of the Shelly device # SHELLY_HOST (required) IP or hostname of the Shelly device
# SHELLY_PASSWORD (optional) Admin password for digest authentication. # SHELLY_PASSWORD (optional) Admin password for digest authentication.
# Omit if auth is disabled on the device. # Omit if auth is disabled on the device.
# SHELLY_USER (optional) Username for auth. Default: admin # SHELLY_USER (optional) Username for auth. Default: admin
# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot. # SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot.
# Default: 1 (reboot after upload) # Default: 1 (reboot after upload)
# SHELLY_SCHEME (optional) Scheme used to call the API: http or https
# Default: http
# #
# Requirements: # Requirements:
# - Shelly Gen3+ device (Gen4 recommended) # - Shelly Gen3+ device (Gen4 recommended)
@@ -23,7 +34,7 @@
# - curl or wget # - curl or wget
# - openssl (for SHA-256 digest and random cnonce) # - openssl (for SHA-256 digest and random cnonce)
# #
# The device must be reachable via HTTP on the local network. # The device must be reachable on the local network.
# The hook uploads the fullchain.pem and private key, # The hook uploads the fullchain.pem and private key,
# then reboots the device to apply the new certificate. # then reboots the device to apply the new certificate.
# #
@@ -52,11 +63,13 @@ shelly_deploy() {
_getdeployconf SHELLY_PASSWORD _getdeployconf SHELLY_PASSWORD
_getdeployconf SHELLY_USER _getdeployconf SHELLY_USER
_getdeployconf SHELLY_REBOOT _getdeployconf SHELLY_REBOOT
_getdeployconf SHELLY_SCHEME
_debug SHELLY_HOST "$SHELLY_HOST" _debug SHELLY_HOST "$SHELLY_HOST"
_debug SHELLY_USER "$SHELLY_USER" _debug SHELLY_USER "$SHELLY_USER"
_secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD" _secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD"
_debug SHELLY_REBOOT "$SHELLY_REBOOT" _debug SHELLY_REBOOT "$SHELLY_REBOOT"
_debug SHELLY_SCHEME "$SHELLY_SCHEME"
if [ -z "$SHELLY_HOST" ]; then if [ -z "$SHELLY_HOST" ]; then
_err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device." _err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device."
@@ -65,11 +78,20 @@ shelly_deploy() {
SHELLY_USER="${SHELLY_USER:-admin}" SHELLY_USER="${SHELLY_USER:-admin}"
SHELLY_REBOOT="${SHELLY_REBOOT:-1}" SHELLY_REBOOT="${SHELLY_REBOOT:-1}"
SHELLY_SCHEME="${SHELLY_SCHEME:-http}"
case "$SHELLY_SCHEME" in
http | https) ;;
*)
_err "SHELLY_SCHEME must be http or https"
return 1
;;
esac
_savedeployconf SHELLY_HOST "$SHELLY_HOST" _savedeployconf SHELLY_HOST "$SHELLY_HOST"
_savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD" _savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD"
_savedeployconf SHELLY_USER "$SHELLY_USER" _savedeployconf SHELLY_USER "$SHELLY_USER"
_savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT" _savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT"
_savedeployconf SHELLY_SCHEME "$SHELLY_SCHEME"
# --- Auth handshake (only if password is set) --- # --- Auth handshake (only if password is set) ---
_shelly_auth_header="" _shelly_auth_header=""
@@ -126,7 +148,7 @@ _shelly_handshake() {
# Use a protected method (Shelly.GetStatus) to trigger 401. # Use a protected method (Shelly.GetStatus) to trigger 401.
# Shelly.GetDeviceInfo is excluded from auth and would miss the challenge. # Shelly.GetDeviceInfo is excluded from auth and would miss the challenge.
_post '{"id":1,"method":"Shelly.GetStatus"}' \ _post '{"id":1,"method":"Shelly.GetStatus"}' \
"http://${SHELLY_HOST}/rpc" "" "" "application/json" "${SHELLY_SCHEME}://${SHELLY_HOST}/rpc" "" "" "application/json"
# Detect auth from HTTP status line rather than response body # Detect auth from HTTP status line rather than response body
if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then
@@ -210,7 +232,7 @@ _shelly_rpc() {
export _H1="" export _H1=""
fi fi
_post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json" _post "$_shelly_body" "${SHELLY_SCHEME}://${SHELLY_HOST}/rpc" "" "" "application/json"
_shelly_ret=$? _shelly_ret=$?
if [ "$_shelly_ret" != "0" ]; then if [ "$_shelly_ret" != "0" ]; then
+40 -14
View File
@@ -189,6 +189,19 @@ ssh_deploy() {
_info "Required commands batched and sent in single call to remote host" _info "Required commands batched and sent in single call to remote host"
fi fi
# With a POSIX remote shell, steps are joined with && so that the first
# failed step stops the rest (no REMOTE_CMD after a failed write). Other
# shells ("powershell -c", "cmd /c") keep the plain ";" join.
_ssh_shell_name="${DEPLOY_SSH_REMOTE_SHELL%% *}"
case "${_ssh_shell_name##*/}" in
sh | bash | dash | ash | ksh | mksh | zsh)
_ssh_fail_fast="yes"
;;
*)
_ssh_fail_fast="no"
;;
esac
_returnCode=0 _returnCode=0
_deploy_ssh_servers="$DEPLOY_SSH_SERVER" _deploy_ssh_servers="$DEPLOY_SSH_SERVER"
for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do
@@ -228,9 +241,9 @@ _ssh_deploy() {
_backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')" _backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')"
# run cleanup on the backup directory, erase all older # run cleanup on the backup directory, erase all older
# than 180 days (15552000 seconds). # than 180 days (15552000 seconds).
_cmdstr="{ now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \ _ssh_add_step "now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \
do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \ do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \
then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr" then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done"
# Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr" # Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr"
_info "Backup of old certificate files will be placed in remote directory $_backupdir" _info "Backup of old certificate files will be placed in remote directory $_backupdir"
_info "Backup directories erased after 180 days." _info "Backup directories erased after 180 days."
@@ -245,7 +258,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
if [ -n "$DEPLOY_SSH_KEYFILE" ]; then if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# backup file we are about to overwrite. # backup file we are about to overwrite.
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi;" _ssh_add_step "if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code return $_err_code
@@ -261,10 +274,9 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
return $_err_code return $_err_code
fi fi
else else
# If file doesn't exist, create it and change its permissions. # If file doesn't exist, create it and change its permissions, then
_cmdstr="$_cmdstr test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE;" # ssh echo to the file. One step: "test ! -f" fails if the file exists.
# ssh echo to the file _ssh_add_step "test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE; echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE"
_cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;"
_info "will copy private key to remote file $DEPLOY_SSH_KEYFILE" _info "will copy private key to remote file $DEPLOY_SSH_KEYFILE"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then if ! _ssh_remote_cmd "$_cmdstr"; then
@@ -282,7 +294,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
_pipe=">>" _pipe=">>"
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# backup file we are about to overwrite. # backup file we are about to overwrite.
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi;" _ssh_add_step "if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code return $_err_code
@@ -304,7 +316,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
fi fi
else else
# ssh echo to the file # ssh echo to the file
_cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE;" _ssh_add_step "echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE"
_info "will copy certificate to remote file $DEPLOY_SSH_CERTFILE" _info "will copy certificate to remote file $DEPLOY_SSH_CERTFILE"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then if ! _ssh_remote_cmd "$_cmdstr"; then
@@ -323,7 +335,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
_pipe=">>" _pipe=">>"
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# backup file we are about to overwrite. # backup file we are about to overwrite.
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi;" _ssh_add_step "if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code return $_err_code
@@ -348,7 +360,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
fi fi
else else
# ssh echo to the file # ssh echo to the file
_cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE;" _ssh_add_step "echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE"
_info "will copy CA file to remote file $DEPLOY_SSH_CAFILE" _info "will copy CA file to remote file $DEPLOY_SSH_CAFILE"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then if ! _ssh_remote_cmd "$_cmdstr"; then
@@ -368,7 +380,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
_pipe=">>" _pipe=">>"
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# backup file we are about to overwrite. # backup file we are about to overwrite.
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi;" _ssh_add_step "if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code return $_err_code
@@ -396,7 +408,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
fi fi
else else
# ssh echo to the file # ssh echo to the file
_cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN;" _ssh_add_step "echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN"
_info "will copy fullchain to remote file $DEPLOY_SSH_FULLCHAIN" _info "will copy fullchain to remote file $DEPLOY_SSH_FULLCHAIN"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then if ! _ssh_remote_cmd "$_cmdstr"; then
@@ -419,7 +431,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
fi fi
if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then
_cmdstr="$_cmdstr $DEPLOY_SSH_REMOTE_CMD;" _ssh_add_step "$DEPLOY_SSH_REMOTE_CMD"
_info "Will execute remote command $DEPLOY_SSH_REMOTE_CMD" _info "Will execute remote command $DEPLOY_SSH_REMOTE_CMD"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then if ! _ssh_remote_cmd "$_cmdstr"; then
@@ -439,6 +451,20 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
return 0 return 0
} }
#step
_ssh_add_step() {
if [ "$_ssh_fail_fast" = "yes" ]; then
if [ -n "$_cmdstr" ]; then
_cmdstr="$_cmdstr && "
fi
# the newline ends the step even if it ends with a comment, ";" or "&"
_cmdstr="$_cmdstr{ $1
}"
else
_cmdstr="$_cmdstr $1;"
fi
}
#cmd #cmd
_ssh_remote_cmd() { _ssh_remote_cmd() {
_cmd="$1" _cmd="$1"
+34 -7
View File
@@ -34,6 +34,8 @@
# interactive input the device name # interactive input the device name
# - export SYNO_DEVICE_ID="" - (deprecated, auth with OTP code instead) # - export SYNO_DEVICE_ID="" - (deprecated, auth with OTP code instead)
# required for omitting 2FA-OTP # required for omitting 2FA-OTP
# - export SYNO_USE_TOKEN=0 - disable SynoToken authentication
# when deploying to Synology SRM
# 3. Run command: # 3. Run command:
# acme.sh --deploy --deploy-hook synology_dsm -d example.com # acme.sh --deploy --deploy-hook synology_dsm -d example.com
################################################################################ ################################################################################
@@ -150,6 +152,20 @@ synology_dsm_deploy() {
encoded_username="$(printf "%s" "$SYNO_USERNAME" | _url_encode)" encoded_username="$(printf "%s" "$SYNO_USERNAME" | _url_encode)"
encoded_password="$(printf "%s" "$SYNO_PASSWORD" | _url_encode)" encoded_password="$(printf "%s" "$SYNO_PASSWORD" | _url_encode)"
# Configure SynoToken authentication (DSM only)
_getdeployconf SYNO_USE_TOKEN
_check2cleardeployconfexp SYNO_USE_TOKEN
_savedeployconf SYNO_USE_TOKEN "$SYNO_USE_TOKEN"
_syno_use_token=1
[ "$SYNO_USE_TOKEN" = "0" ] && _syno_use_token=""
if [ -n "$_syno_use_token" ]; then
enable_token_param="&enable_syno_token=yes"
else
enable_token_param=""
fi
_debug enable_token_param "$enable_token_param"
# ## START ## - DEPRECATED, for backward compatibility # ## START ## - DEPRECATED, for backward compatibility
_getdeployconf SYNO_TOTP_SECRET _getdeployconf SYNO_TOTP_SECRET
@@ -173,7 +189,7 @@ synology_dsm_deploy() {
_debug3 H1 "${_H1}" _debug3 H1 "${_H1}"
fi fi
response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version&enable_syno_token=yes&otp_code=$DEPRECATED_otp_code&device_name=certrenewal&device_id=$SYNO_DEVICE_ID" "$_base_url/webapi/$api_path?enable_syno_token=yes") response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version$enable_token_param&otp_code=$DEPRECATED_otp_code&device_name=certrenewal&device_id=$SYNO_DEVICE_ID" "$_base_url/webapi/$api_path${enable_token_param:+?enable_syno_token=yes}")
_debug3 response "$response" _debug3 response "$response"
# ## END ## - DEPRECATED, for backward compatibility # ## END ## - DEPRECATED, for backward compatibility
# If SYNO_DEVICE_ID or SYNO_OTP_CODE is set, we treat current account enabled 2FA-OTP. # If SYNO_DEVICE_ID or SYNO_OTP_CODE is set, we treat current account enabled 2FA-OTP.
@@ -225,7 +241,7 @@ synology_dsm_deploy() {
otp_enforce_option="" otp_enforce_option=""
fi fi
fi fi
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes") response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password$enable_token_param")
if [ -n "$SYNO_USE_TEMP_ADMIN" ] && [ -n "$otp_enforce_option" ]; then if [ -n "$SYNO_USE_TEMP_ADMIN" ] && [ -n "$otp_enforce_option" ]; then
synosetkeyvalue /etc/synoinfo.conf otp_enforce_option "$otp_enforce_option" synosetkeyvalue /etc/synoinfo.conf otp_enforce_option "$otp_enforce_option"
_info "Restored previous enforce 2FA-OTP option." _info "Restored previous enforce 2FA-OTP option."
@@ -247,7 +263,7 @@ synology_dsm_deploy() {
if [ -n "$SYNO_DEVICE_ID" ]; then if [ -n "$SYNO_DEVICE_ID" ]; then
# Omit OTP code with SYNO_DEVICE_ID. # Omit OTP code with SYNO_DEVICE_ID.
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&device_name=$SYNO_DEVICE_NAME&device_id=$SYNO_DEVICE_ID") response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password$enable_token_param&device_name=$SYNO_DEVICE_NAME&device_id=$SYNO_DEVICE_ID")
_secure_debug3 response "$response" _secure_debug3 response "$response"
else else
# Require the OTP code if still unset. # Require the OTP code if still unset.
@@ -260,7 +276,7 @@ synology_dsm_deploy() {
if [ -z "$SYNO_OTP_CODE" ]; then if [ -z "$SYNO_OTP_CODE" ]; then
response='{"error":{"code":404}}' response='{"error":{"code":404}}'
else else
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&enable_device_token=yes&device_name=$SYNO_DEVICE_NAME&otp_code=$SYNO_OTP_CODE") response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password$enable_token_param&enable_device_token=yes&device_name=$SYNO_DEVICE_NAME&otp_code=$SYNO_OTP_CODE")
_secure_debug3 response "$response" _secure_debug3 response "$response"
id_property='device_id' id_property='device_id'
@@ -302,14 +318,18 @@ synology_dsm_deploy() {
token=$(echo "$response" | grep "synotoken" | sed -n 's/.*"synotoken" *: *"\([^"]*\).*/\1/p') token=$(echo "$response" | grep "synotoken" | sed -n 's/.*"synotoken" *: *"\([^"]*\).*/\1/p')
_debug "Session ID" "$sid" _debug "Session ID" "$sid"
_debug SynoToken "$token" _debug SynoToken "$token"
if [ -z "$sid" ] || [ -z "$token" ]; then if [ -z "$sid" ] || { [ -n "$_syno_use_token" ] && [ -z "$token" ]; }; then
# Still can't get necessary info even got no errors, may Synology have API updated? # Still can't get necessary info even got no errors, may Synology have API updated?
_err "Unable to authenticate to $_base_url, you may report this by providing full log with '--debug 3'." _err "Unable to authenticate to $_base_url, you may report this by providing full log with '--debug 3'."
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1 return 1
fi fi
_H1="X-SYNO-TOKEN: $token" if [ -n "$_syno_use_token" ]; then
_H1="X-SYNO-TOKEN: $token"
else
_H1=""
fi
export _H1 export _H1
_debug2 H1 "${_H1}" _debug2 H1 "${_H1}"
@@ -381,8 +401,15 @@ synology_dsm_deploy() {
content="$(printf "%b_" "$content")" content="$(printf "%b_" "$content")"
content="${content%_}" # protect trailing \n content="${content%_}" # protect trailing \n
if [ -n "$_syno_use_token" ]; then
token_param="&SynoToken=$token"
else
token_param=""
fi
_debug token_param "$token_param"
_info "Upload certificate to the Synology DSM." _info "Upload certificate to the Synology DSM."
response=$(_post "$content" "$_base_url/webapi/entry.cgi?api=SYNO.Core.Certificate&method=import&version=1&SynoToken=$token&_sid=$sid" "" "POST" "multipart/form-data; boundary=${delim}") response=$(_post "$content" "$_base_url/webapi/entry.cgi?api=SYNO.Core.Certificate&method=import&version=1$token_param&_sid=$sid" "" "POST" "multipart/form-data; boundary=${delim}")
_debug3 response "$response" _debug3 response "$response"
if ! echo "$response" | grep '"error":' >/dev/null; then if ! echo "$response" | grep '"error":' >/dev/null; then
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_abion_info='Abion.com
Site: abion.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_abion
Options:
ABION_API_KEY API Key. Created in the Abion Core web interface.
Issues: github.com/acmesh-official/acme.sh/issues/7325
'
ABION_Api_Endpoint="https://api.abion.com/v1"
##################### Public functions #####################
# Usage: dns_abion_add <domain> <txt record>
# Example: dns_abion_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_abion_add() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue=$2
ABION_API_KEY="${ABION_API_KEY:-$(_readaccountconf_mutable ABION_API_KEY)}"
if [ -z "$ABION_API_KEY" ]; then
ABION_API_KEY=""
_err "You need to specify ABION_API_KEY."
return 1
fi
_saveaccountconf_mutable ABION_API_KEY "$ABION_API_KEY"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
if ! _abion_get_txt_records; then
return 1
fi
if _contains "$_txt_records" "\"rdata\":\"$txtvalue\""; then
_info "TXT record already exists"
return 0
fi
_records=$(echo "$_txt_records" | tr '\n' ',' | sed 's/,*$//')
if [ -n "$_records" ]; then
_records="$_records,"
fi
_info "Adding record"
if ! _abion_patch_txt "[$_records{\"rdata\":\"$txtvalue\",\"ttl\":60}]"; then
return 1
fi
_info "Added, OK"
return 0
}
# Usage: dns_abion_rm <domain> <txt record>
dns_abion_rm() {
fulldomain="$(echo "$1" | _lower_case)"
txtvalue=$2
ABION_API_KEY="${ABION_API_KEY:-$(_readaccountconf_mutable ABION_API_KEY)}"
if [ -z "$ABION_API_KEY" ]; then
ABION_API_KEY=""
_err "You need to specify ABION_API_KEY."
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
if ! _abion_get_txt_records; then
return 1
fi
if ! _contains "$_txt_records" "\"rdata\":\"$txtvalue\""; then
_info "TXT record not found, nothing to remove"
return 0
fi
_records=$(echo "$_txt_records" | grep -v "\"rdata\":\"$txtvalue\"" | tr '\n' ',' | sed 's/,*$//')
if [ -n "$_records" ]; then
_records="[$_records]"
else
_records="null"
fi
_info "Deleting record"
if ! _abion_patch_txt "$_records"; then
return 1
fi
_info "Deleted, OK"
return 0
}
##################### Private functions #####################
_get_root() {
domain=$1
i=1
p=0
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
return 1
fi
if ! _abion_rest GET "zones/$h"; then
return 1
fi
if _contains "$response" '"type" *: *"zone"'; then
if [ "$p" = "0" ]; then
_sub_domain="@"
else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
fi
_domain="$h"
return 0
fi
if ! _contains "$response" "Zone not found"; then
_err "$response"
return 1
fi
p="$i"
i=$(_math "$i" + 1)
done
}
_abion_get_txt_records() {
if ! _abion_rest GET "zones/$_domain"; then
return 1
fi
_sub_re=$(printf "%s" "$_sub_domain" | sed 's/[.]/\\./g')
_txt_records=$(echo "$response" | _normalizeJson |
sed -n "s/.*\"$_sub_re\":{//p" |
sed 's/\] *}.*/]/' |
_egrep_o '"TXT":\[[^]]*\]' |
sed 's/^"TXT":\[ *//;s/ *\]$//;s/} *, *{/}\
{/g')
_debug2 _txt_records "$_txt_records"
return 0
}
_abion_patch_txt() {
if ! _abion_rest PATCH "zones/$_domain" "{\"data\":{\"type\":\"zone\",\"id\":\"$_domain\",\"attributes\":{\"records\":{\"$_sub_domain\":{\"TXT\":$1}}}}}"; then
return 1
fi
if ! _contains "$response" '"type" *: *"zone"'; then
_err "Unexpected API response: $response"
return 1
fi
return 0
}
_abion_rest() {
method=$1
endpoint=$2
data=$3
export _H1="X-API-KEY: $ABION_API_KEY"
export _H2="Content-Type: application/json"
if [ "$method" = "GET" ]; then
response="$(_get "$ABION_Api_Endpoint/$endpoint")"
else
response="$(_post "$data" "$ABION_Api_Endpoint/$endpoint" "" "$method")"
fi
if [ "$?" != "0" ]; then
_err "error $endpoint"
return 1
fi
_debug2 response "$response"
return 0
}
+1 -1
View File
@@ -120,7 +120,7 @@ _baidu_run_with_fallback() {
} }
_baidu_engine_order() { _baidu_engine_order() {
_pref="$(_lower_case "$(_baidu_trim_ws "${Baidu_API_Preference:-auto}")")" _pref="$(_baidu_trim_ws "${Baidu_API_Preference:-auto}" | _lower_case)"
case "$_pref" in case "$_pref" in
legacy) legacy)
printf "%s" "legacy new" printf "%s" "legacy new"
+348
View File
@@ -0,0 +1,348 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_bergdns_info='bergdns.at
Site: bergdns.at
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bergdns
Options:
BERGDNS_API_KEY API key, as issued in the account UI. Needs read (to find the zone and the record) and write over the challenge names.
BERGDNS_API_URL API base URL. Optional. Default "https://bergdns.at/v1".
BERGDNS_TTL TTL of the challenge record, in seconds. Optional. Default "60".
BERGDNS_PROPAGATION_TIMEOUT Seconds to wait for the record to reach every secondary. Optional. Default "60". "0" does not wait.
Issues: github.com/acmesh-official/acme.sh/issues/7261
Author: Kenny Kropp <https://github.com/kekropp>
'
_BERGDNS_DEFAULT_URL='https://bergdns.at/v1'
_BERGDNS_DEFAULT_TTL='60'
_BERGDNS_DEFAULT_WAIT='60'
######## Public functions ####################################################
# Usage: dns_bergdns_add _acme-challenge.www.example.com "token"
# The value is added to the RRset, so a domain and its wildcard can be
# validated at the same time.
dns_bergdns_add() {
fulldomain=$(echo "$1" | _lower_case)
txtvalue=$2
_bergdns_init || return 1
_bergdns_find_zone "$fulldomain" || return 1
_bergdns_find_rrset "$fulldomain" || return 1
_info "Adding TXT $fulldomain in zone $_bergdns_zone_name"
if [ -z "$_bergdns_rrset_id" ]; then
if _bergdns_rest POST "zones/$_bergdns_zone_id/rrsets" \
"{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":$BERGDNS_TTL,\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
_bergdns_rrset_id=$(echo "$response" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_debug _bergdns_rrset_id "$_bergdns_rrset_id"
_bergdns_wait "$fulldomain"
return 0
fi
if [ "$_bergdns_code" != "rrset_exists" ]; then
_err "bergdns: could not add the challenge record: $_bergdns_error"
return 1
fi
# another run created it since the lookup above
_bergdns_find_rrset "$fulldomain" || return 1
if [ -z "$_bergdns_rrset_id" ]; then
_err "bergdns: could not find the challenge record at $fulldomain"
return 1
fi
fi
if ! _bergdns_rest POST \
"zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \
"{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
_err "bergdns: could not add the challenge record: $_bergdns_error"
return 1
fi
_bergdns_wait "$fulldomain"
}
# Usage: dns_bergdns_rm _acme-challenge.www.example.com "token"
# Only this value is removed. Removing the last value deletes the RRset, and
# removing a value that does not exist is not an error.
dns_bergdns_rm() {
fulldomain=$(echo "$1" | _lower_case)
txtvalue=$2
_bergdns_init || return 1
_bergdns_find_zone "$fulldomain" || return 1
_bergdns_find_rrset "$fulldomain" || return 1
if [ -z "$_bergdns_rrset_id" ]; then
_info "bergdns: no TXT records at $fulldomain, nothing to remove"
return 0
fi
_info "Removing TXT $fulldomain from zone $_bergdns_zone_name"
if ! _bergdns_rest DELETE \
"zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \
"{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
# Any flavour of not-found is a cleanup that has already happened: the
# RRset was removed by a previous run, or by the other half of a
# domain-and-wildcard pair taking the last value with it.
if [ "$_bergdns_status" = "404" ]; then
_info "bergdns: $fulldomain holds no such record any more, nothing to remove"
return 0
fi
_err "bergdns: could not remove the challenge record: $_bergdns_error"
return 1
fi
return 0
}
######## Private functions ###################################################
_bergdns_init() {
BERGDNS_API_KEY="${BERGDNS_API_KEY:-$(_readaccountconf_mutable BERGDNS_API_KEY)}"
BERGDNS_API_URL="${BERGDNS_API_URL:-$(_readaccountconf_mutable BERGDNS_API_URL)}"
BERGDNS_TTL="${BERGDNS_TTL:-$(_readaccountconf_mutable BERGDNS_TTL)}"
BERGDNS_PROPAGATION_TIMEOUT="${BERGDNS_PROPAGATION_TIMEOUT:-$(_readaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT)}"
if [ -z "$BERGDNS_API_KEY" ]; then
BERGDNS_API_KEY=""
_clearaccountconf_mutable BERGDNS_API_KEY
_err "You have not set BERGDNS_API_KEY. Create a key in the bergdns UI and export it:"
_err " export BERGDNS_API_KEY=\"bgd_...\""
return 1
fi
[ -n "$BERGDNS_API_URL" ] || BERGDNS_API_URL="$_BERGDNS_DEFAULT_URL"
[ -n "$BERGDNS_TTL" ] || BERGDNS_TTL="$_BERGDNS_DEFAULT_TTL"
[ -n "$BERGDNS_PROPAGATION_TIMEOUT" ] || BERGDNS_PROPAGATION_TIMEOUT="$_BERGDNS_DEFAULT_WAIT"
# strip trailing slashes
BERGDNS_API_URL=$(echo "$BERGDNS_API_URL" | sed 's#/*$##')
# The TTL is interpolated into the request body and the timeout is counted
# down in arithmetic, so a stray value from the environment or from an old
# account.conf has to be caught here rather than become malformed JSON and
# an opaque 400.
case "$BERGDNS_TTL" in
*[!0-9]* | '')
_err "bergdns: BERGDNS_TTL must be a number of seconds, not \"$BERGDNS_TTL\"."
return 1
;;
esac
case "$BERGDNS_PROPAGATION_TIMEOUT" in
*[!0-9]* | '')
_err "bergdns: BERGDNS_PROPAGATION_TIMEOUT must be a number of seconds, not \"$BERGDNS_PROPAGATION_TIMEOUT\"."
return 1
;;
esac
_saveaccountconf_mutable BERGDNS_API_KEY "$BERGDNS_API_KEY"
# Only what the user actually chose is written back, and a value equal to
# the default clears any older setting. Persisting a default would pin the
# install to today's value, and a later change to the shipped one -- a move
# of the API base above all -- would never reach it; leaving an old setting
# in place would mean the environment could never put one back to default.
if [ "$BERGDNS_API_URL" = "$_BERGDNS_DEFAULT_URL" ]; then
_clearaccountconf_mutable BERGDNS_API_URL
else
_saveaccountconf_mutable BERGDNS_API_URL "$BERGDNS_API_URL"
fi
if [ "$BERGDNS_TTL" = "$_BERGDNS_DEFAULT_TTL" ]; then
_clearaccountconf_mutable BERGDNS_TTL
else
_saveaccountconf_mutable BERGDNS_TTL "$BERGDNS_TTL"
fi
if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "$_BERGDNS_DEFAULT_WAIT" ]; then
_clearaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT
else
_saveaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT "$BERGDNS_PROPAGATION_TIMEOUT"
fi
return 0
}
# Usage: _bergdns_find_zone _acme-challenge.www.example.com
# Sets _bergdns_zone_id and _bergdns_zone_name.
# Zones are addressed by an id, not by name, so the zone list is fetched once
# and the longest matching zone name wins.
_bergdns_find_zone() {
_bergdns_fqdn=$1
_bergdns_zone_id=""
_bergdns_zone_name=""
if ! _bergdns_rest GET "zones"; then
_err "bergdns: could not list zones: $_bergdns_error"
return 1
fi
# one zone object per line, so id and name stay together
_bergdns_zone_lines=$(echo "$response" | tr '{' '\n')
_bergdns_cand="$_bergdns_fqdn"
while [ -n "$_bergdns_cand" ]; do
_bergdns_line=$(echo "$_bergdns_zone_lines" | _bergdns_select "$_bergdns_cand" | _head_n 1)
if [ -n "$_bergdns_line" ]; then
_bergdns_zone_id=$(echo "$_bergdns_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_bergdns_zone_name="$_bergdns_cand"
_debug _bergdns_zone_id "$_bergdns_zone_id"
_debug _bergdns_zone_name "$_bergdns_zone_name"
[ -n "$_bergdns_zone_id" ] && return 0
break
fi
case "$_bergdns_cand" in
*.*) _bergdns_cand=${_bergdns_cand#*.} ;;
*) _bergdns_cand="" ;;
esac
done
_err "bergdns: no zone in this account holds $_bergdns_fqdn."
_err "bergdns: the key must be able to read the zone as well as write the record."
return 1
}
# Usage: _bergdns_find_rrset _acme-challenge.www.example.com
# Sets _bergdns_rrset_id to the id of the TXT RRset at that name, or to an
# empty string if there is none. Records are addressed by id, so the zone's
# RRsets are listed to find it.
_bergdns_find_rrset() {
_bergdns_fqdn=$1
_bergdns_rrset_id=""
if ! _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets"; then
_err "bergdns: could not list the records of $_bergdns_zone_name: $_bergdns_error"
return 1
fi
_bergdns_line=$(echo "$response" | tr '{' '\n' | _bergdns_select "$_bergdns_fqdn" TXT | _head_n 1)
if [ -n "$_bergdns_line" ]; then
_bergdns_rrset_id=$(echo "$_bergdns_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
fi
_debug _bergdns_rrset_id "$_bergdns_rrset_id"
return 0
}
# Usage: ... | _bergdns_select name [type]
# Reads one JSON object per line and prints those whose "name" is name and,
# when a type is given, whose "type" is that type. The two fields are matched
# one at a time, so neither the order the server writes its keys in nor
# anything sitting between them changes the answer.
#
# The comparison is a shell case, which is literal by construction: grep -F
# does not exist on Solaris, and _contains and _startswith would read the name
# as a regular expression. Each pattern anchors on the start of the object or
# on the comma before the key, so a key that merely ends in "name" cannot
# match.
_bergdns_select() {
_bergdns_sel_name=$1
_bergdns_sel_type=$2
while IFS= read -r _bergdns_sel_line || [ -n "$_bergdns_sel_line" ]; do
case "$_bergdns_sel_line" in
'"name":"'"$_bergdns_sel_name"'"'* | *',"name":"'"$_bergdns_sel_name"'"'*) ;;
*) continue ;;
esac
if [ -n "$_bergdns_sel_type" ]; then
case "$_bergdns_sel_line" in
'"type":"'"$_bergdns_sel_type"'"'* | *',"type":"'"$_bergdns_sel_type"'"'*) ;;
*) continue ;;
esac
fi
printf '%s\n' "$_bergdns_sel_line"
done
}
# Usage: _bergdns_wait _acme-challenge.www.example.com
# Polls the propagation endpoint until all bergdns nameservers serve the
# record. A timeout is logged but does not fail the issuance.
#
# This covers the zone transfer from the primary to the secondaries, which
# takes seconds; the resolver side is acme.sh's own _check_dns_entries, which
# runs after every record has been added and has a timeout of its own.
_bergdns_wait() {
_bergdns_fqdn=$1
if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "0" ] || [ -z "$_bergdns_rrset_id" ]; then
return 0
fi
_bergdns_waited=0
while [ "$_bergdns_waited" -lt "$BERGDNS_PROPAGATION_TIMEOUT" ]; do
if _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/propagation"; then
case "$response" in
*'"propagated":true'*)
_info "bergdns: $_bergdns_fqdn is served by every secondary after ${_bergdns_waited}s"
return 0
;;
esac
elif [ "$_bergdns_code" = "propagation_unavailable" ]; then
# propagation checks are not configured on this server
_info "bergdns: this deployment does not offer propagation checks; not waiting"
return 0
else
case "$_bergdns_status" in
429) ;; # rate limited, worth another go
4*)
# The check is refused rather than pending, and waiting will not
# change that. _check_dns_entries still has to pass, so this is not
# the place to fail the issuance.
_info "bergdns: the propagation check is unavailable ($_bergdns_error); not waiting"
return 0
;;
esac
fi
_sleep 5
_bergdns_waited=$((_bergdns_waited + 5))
done
_info "bergdns: $_bergdns_fqdn was not on every secondary after ${BERGDNS_PROPAGATION_TIMEOUT}s; continuing anyway"
return 0
}
# Usage: _bergdns_rest method endpoint [body]
# Sets response and _bergdns_status. On failure also sets _bergdns_error and,
# where the API itself answered, _bergdns_code.
_bergdns_rest() {
_bergdns_method=$1
_bergdns_endpoint=$2
_bergdns_body=$3
_bergdns_error=""
_bergdns_code=""
_bergdns_status=""
export _H1="Authorization: Bearer $BERGDNS_API_KEY"
export _H2="Accept: application/json"
_bergdns_url="$BERGDNS_API_URL/$_bergdns_endpoint"
_debug _bergdns_url "$_bergdns_url"
# drop the headers of the previous request, so that a request which never
# reaches the server cannot be read as carrying its status
if [ -f "$HTTP_HEADER" ]; then
: >"$HTTP_HEADER"
fi
if [ "$_bergdns_method" = "GET" ]; then
response="$(_get "$_bergdns_url")"
else
_debug2 _bergdns_body "$_bergdns_body"
response="$(_post "$_bergdns_body" "$_bergdns_url" "" "$_bergdns_method" "application/json")"
fi
_bergdns_ret="$?"
_debug2 response "$response"
if [ "$_bergdns_ret" != "0" ]; then
_bergdns_error="the request to $_bergdns_url could not be made"
return 1
fi
_bergdns_status="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug _bergdns_status "$_bergdns_status"
# The HTTP status decides. Errors from the API itself are RFC 9457
# problem+json and carry a "detail" to show and a stable "code" to branch on,
# but a request that never gets that far -- bergdns.at answers from behind a
# reverse proxy, whose 502 and 504 are HTML -- has neither, and reading the
# body alone would take those for success.
case "$_bergdns_status" in
2*) return 0 ;;
esac
_bergdns_error=$(echo "$response" | _egrep_o '"detail":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_bergdns_code=$(echo "$response" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
[ -n "$_bergdns_error" ] || _bergdns_error="$_bergdns_url answered HTTP ${_bergdns_status:-(none)}"
_debug _bergdns_code "$_bergdns_code"
return 1
}
+1 -1
View File
@@ -283,7 +283,7 @@ _bhosted_response_has_error() {
fi fi
# Fallback for unexpected/non-XML responses # Fallback for unexpected/non-XML responses
_resp_lc="$(_lower_case "$_resp")" _resp_lc="$(printf "%s\n" "$_resp" | _lower_case)"
if _contains "$_resp_lc" "error"; then if _contains "$_resp_lc" "error"; then
_debug "Detected 'error' in response" _debug "Detected 'error' in response"
+2 -2
View File
@@ -18,7 +18,7 @@ CF_Api="https://api.cloudflare.com/client/v4"
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" #Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_cf_add() { dns_cf_add() {
fulldomain=$1 fulldomain="$(_idn "$1")"
txtvalue=$2 txtvalue=$2
CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}" CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
@@ -109,7 +109,7 @@ dns_cf_add() {
#fulldomain txtvalue #fulldomain txtvalue
dns_cf_rm() { dns_cf_rm() {
fulldomain=$1 fulldomain="$(_idn "$1")"
txtvalue=$2 txtvalue=$2
CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}" CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
+10 -5
View File
@@ -15,7 +15,7 @@ DNSimple_API="https://api.dnsimple.com/v2"
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_dnsimple_add() { dns_dnsimple_add() {
fulldomain=$1 fulldomain=$(echo "$1" | _lower_case)
txtvalue=$2 txtvalue=$2
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}" DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
@@ -56,7 +56,7 @@ dns_dnsimple_add() {
# fulldomain # fulldomain
dns_dnsimple_rm() { dns_dnsimple_rm() {
fulldomain=$1 fulldomain=$(echo "$1" | _lower_case)
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}" DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
@@ -110,9 +110,13 @@ _get_root() {
return 1 return 1
fi fi
if _contains "$response" 'not found'; then if ! _contains "$response" "\"name\":\"$h\""; then
_debug "$h not found" if ! _dnsimple_rest GET "$_account_id/zones/$h/records?per_page=1"; then
else return 1
fi
fi
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"pagination":'; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$previous") _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$previous")
_domain="$h" _domain="$h"
@@ -121,6 +125,7 @@ _get_root() {
return 0 return 0
fi fi
_debug "$h not found"
previous="$i" previous="$i"
i=$(_math "$i" + 1) i=$(_math "$i" + 1)
+21
View File
@@ -59,6 +59,13 @@ dns_gandi_livedns_rm() {
fulldomain=$1 fulldomain=$1
txtvalue=$2 txtvalue=$2
GANDI_LIVEDNS_KEY="${GANDI_LIVEDNS_KEY:-$(_readaccountconf_mutable GANDI_LIVEDNS_KEY)}"
GANDI_LIVEDNS_TOKEN="${GANDI_LIVEDNS_TOKEN:-$(_readaccountconf_mutable GANDI_LIVEDNS_TOKEN)}"
if [ -z "$GANDI_LIVEDNS_KEY" ] && [ -z "$GANDI_LIVEDNS_TOKEN" ]; then
_err "No Token or API key (deprecated) specified for Gandi LiveDNS."
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"
@@ -80,6 +87,20 @@ dns_gandi_livedns_rm() {
_new_rrset_values=$(echo "$_new_rrset_values" | sed "s/\[ *,/\[/g") _new_rrset_values=$(echo "$_new_rrset_values" | sed "s/\[ *,/\[/g")
_debug "New rrset_values" "$_new_rrset_values" _debug "New rrset_values" "$_new_rrset_values"
# Last value removed: delete the record instead of leaving an empty TXT rrset.
if [ "$(echo "$_new_rrset_values" | tr -d ' ')" = "[]" ]; then
if ! _gandi_livedns_rest DELETE "domains/$_domain/records/$_sub_domain/TXT"; then
return 1
fi
_gandi_livedns_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
if [ "$_gandi_livedns_code" != "204" ]; then
_err "Error deleting record: $response"
return 1
fi
_info "Removing record $(__green "success")"
return 0
fi
_gandi_livedns_rest PUT \ _gandi_livedns_rest PUT \
"domains/$_domain/records/$_sub_domain/TXT" \ "domains/$_domain/records/$_sub_domain/TXT" \
"{\"rrset_ttl\": 300, \"rrset_values\": $_new_rrset_values}" && "{\"rrset_ttl\": 300, \"rrset_values\": $_new_rrset_values}" &&
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_mvmnet_info='mvmnet.com
Site: mvmnet.com
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mvmnet
Api Docs: https://api.mvmnet.com/api/1.0/documentation/
Issues: https://github.com/acmesh-official/acme.sh/issues/7297
Author: Matteo Gaggiano <github.com/marchrius>
Options:
MVMNET_ID application_id
MVMNET_KEY application_key
MVMNET_SEED application_seed
MVMNET_API_URL API base URL (defaults to https://api.mvmnet.com/api/1.0, use https://api.mvmnet.com/ote/1.0 for test)
'
MVMNET_API_URL="${MVMNET_API_URL:-https://api.mvmnet.com/api/1.0}"
######## Public functions #####################
dns_mvmnet_add() {
fulldomain=$1
txtvalue=$2
_mvmnet_retrieve_and_check_variables || return 1
_debug "First detect the root zone"
if ! _mvmnet_get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _domain "$_domain"
_debug _subdomain "$_subdomain"
_info "Adding TXT record to ${fulldomain}"
postdata=$(printf '{"domain":"%s","rtype":"TXT","ldata":"%s","rdata":"%s"}' "${_domain}" "${_subdomain}" "${txtvalue}")
_mvmnet_rest POST "dns/zone/record" "$postdata"
if ! _contains "${response}" 'error'; then
return 0
fi
_err "Could not create resource record, check logs"
_err "${response}"
return 1
}
dns_mvmnet_rm() {
fulldomain=$1
txtvalue=$2
_mvmnet_check_variables || return 1
_debug "First detect the root zone"
if ! _mvmnet_get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _domain "$_domain"
_debug _subdomain "$_subdomain"
if ! _mvmnet_get_record_id "$_subdomain" "$_domain" "$txtvalue"; then
_warn "Record id for $_subdomain not found, please remove it manually"
return 0
fi
_debug _subdomain_record_id "$_subdomain_record_id"
_info "Deleting resource record $fulldomain ($_subdomain_record_id)"
_mvmnet_rest DELETE "dns/zone/record" "domain=${_domain}&id=${_subdomain_record_id}"
if ! _contains "${response}" 'error'; then
return 0
fi
_err "Could not delete resource record, check logs"
_err "${response}"
return 1
}
#################### Private functions below ##################################
_mvmnet_check_variables() {
MVMNET_ID="${MVMNET_ID:-$(_readaccountconf_mutable MVMNET_ID)}"
MVMNET_KEY="${MVMNET_KEY:-$(_readaccountconf_mutable MVMNET_KEY)}"
MVMNET_SEED="${MVMNET_SEED:-$(_readaccountconf_mutable MVMNET_SEED)}"
if [ -z "$MVMNET_ID" ] || [ -z "$MVMNET_KEY" ] || [ -z "$MVMNET_SEED" ]; then
MVMNET_ID=""
MVMNET_KEY=""
MVMNET_SEED=""
_err "You don't specify application_id, application_key or application_seed."
return 1
fi
}
_mvmnet_retrieve_and_check_variables() {
_mvmnet_check_variables || return 1
_saveaccountconf_mutable MVMNET_ID "$MVMNET_ID"
_saveaccountconf_mutable MVMNET_KEY "$MVMNET_KEY"
_saveaccountconf_mutable MVMNET_SEED "$MVMNET_SEED"
}
#_acme-challenge.www.domain.com
#returns
# _subdomain=_acme-challenge.www
# _domain=domain.com
# _domain_id=123456789
_mvmnet_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
_debug "not valid $h"
return 1
fi
if ! _mvmnet_rest GET "domain/info" "domain=$h"; then
return 1
fi
if _contains "$response" "\"domain_idn\":\"$h\""; then
_domain_id=$(echo "$response" | _egrep_o ".\"id\": *\"[0-9]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d " ")
if [ "$_domain_id" ]; then
_subdomain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
fi
return 1
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
# _acme-challenge.www
# domain.com
# value (Optional)
#returns
# _subdomain_record_id=123456789
_mvmnet_get_record_id() {
subdomain=$1
domain=$2
txtvalue=$3
_debug3 subdomain "$subdomain"
_debug3 domain "$domain"
_debug3 txtvalue "$txtvalue"
hosttofind="${subdomain}.${domain}"
if ! _mvmnet_rest GET "dns/zone" "domain=$domain"; then
return 1
fi
objectbody="$(echo "$response" | _egrep_o '\{[^{}]*\}' | grep -E "\"host\" *: *\"$hosttofind\.\".*\"type\" *: *\"TXT\"|\"type\" *: *\"TXT\".*\"host\" *: *\"$hosttofind\.\"")"
_debug3 objectbody "${objectbody}"
if [ "$txtvalue" != "" ]; then
_info "A value will be searched: $txtvalue"
objectbody="$(echo "$objectbody" | grep "\"value\" *: *\"\\\\\"$txtvalue\\\\\"\"")"
_debug3 objectbody "${objectbody}"
fi
_subdomain_record_id="$(echo "$objectbody" | _egrep_o ".\"id\": *\"[0-9]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d ' ')"
if [ "${_subdomain_record_id}x" = "x" ]; then
_err "error retrieve sub domain record id"
return 1
fi
return 0
}
#returns
# response
_mvmnet_rest() {
m=$1
ep="$2"
data="$3"
_debug "$ep"
signature="$(printf "%s" "${MVMNET_ID}+${MVMNET_KEY}+$m+${MVMNET_SEED}" | _digest "sha1" "hex")"
export _H1="X-Mvm-Application: ${MVMNET_ID}"
export _H2="X-Mvm-Signature: ${signature}"
case "$m" in
POST | PUT)
_debug data "$data"
response="$(_post "$data" "$MVMNET_API_URL/$ep" "" "$m" "application/json")"
;;
DELETE)
response="$(_post "" "$MVMNET_API_URL/$ep?$data" "" "$m")"
;;
GET)
response="$(_get "$MVMNET_API_URL/$ep?$data")"
;;
esac
if [ "$?" != "0" ]; then
_err "error $ep"
return 1
fi
_debug response "${response}"
return 0
}
+6 -3
View File
@@ -185,13 +185,14 @@ _get_root_by_getHosts() {
} }
_namecheap_set_publicip() { _namecheap_set_publicip() {
NAMECHEAP_SOURCEIP="${NAMECHEAP_SOURCEIP:-$(_readaccountconf_mutable NAMECHEAP_SOURCEIP)}"
if [ -z "$NAMECHEAP_SOURCEIP" ]; then if [ -z "$NAMECHEAP_SOURCEIP" ]; then
_err "No Source IP specified for Namecheap API." _err "No Source IP specified for Namecheap API."
_err "Use your public ip address or an url to retrieve it (e.g. https://ifconfig.co/ip) and export it as NAMECHEAP_SOURCEIP" _err "Use your public ip address or an url to retrieve it (e.g. https://ifconfig.co/ip) and export it as NAMECHEAP_SOURCEIP"
return 1 return 1
else else
_saveaccountconf NAMECHEAP_SOURCEIP "$NAMECHEAP_SOURCEIP" _saveaccountconf_mutable NAMECHEAP_SOURCEIP "$NAMECHEAP_SOURCEIP"
_debug sourceip "$NAMECHEAP_SOURCEIP" _debug sourceip "$NAMECHEAP_SOURCEIP"
ip=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}') ip=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
@@ -252,6 +253,8 @@ _namecheap_parse_host() {
} }
_namecheap_check_config() { _namecheap_check_config() {
NAMECHEAP_API_KEY="${NAMECHEAP_API_KEY:-$(_readaccountconf_mutable NAMECHEAP_API_KEY)}"
NAMECHEAP_USERNAME="${NAMECHEAP_USERNAME:-$(_readaccountconf_mutable NAMECHEAP_USERNAME)}"
if [ -z "$NAMECHEAP_API_KEY" ]; then if [ -z "$NAMECHEAP_API_KEY" ]; then
_err "No API key specified for Namecheap API." _err "No API key specified for Namecheap API."
@@ -265,8 +268,8 @@ _namecheap_check_config() {
return 1 return 1
fi fi
_saveaccountconf NAMECHEAP_API_KEY "$NAMECHEAP_API_KEY" _saveaccountconf_mutable NAMECHEAP_API_KEY "$NAMECHEAP_API_KEY"
_saveaccountconf NAMECHEAP_USERNAME "$NAMECHEAP_USERNAME" _saveaccountconf_mutable NAMECHEAP_USERNAME "$NAMECHEAP_USERNAME"
return 0 return 0
} }
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_pblin_info='publicinternet.com
Domains: public-internet.co.uk pblin.net
Site: www.publicinternet.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_pblin
Options:
PBLIN_API_KEY API Key. Create an API key in the Public Internet customer portal at https://portal.publicinternet.com. A key is scoped to one domain, so each certificate needs the key for its own domain
Issues: github.com/acmesh-official/acme.sh/issues/7314
Author: Public Internet <https://github.com/publicinternet>
'
PBLIN_Api="https://api.pblin.net/dns"
#Usage: dns_pblin_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_pblin_add() {
fulldomain=$(_idn "$1")
txtvalue=$2
if ! _pblin_init; then
return 1
fi
if ! _get_root "$fulldomain"; then
return 1
fi
#an API key is scoped to one zone, so it is kept per certificate, and only once a zone lookup has accepted it
_savedomainconf SAVED_PBLIN_API_KEY "$PBLIN_API_KEY"
#the out-of-tree dns_pblin.sh saved the key unprefixed, which a renewal would load over a newly exported key
_cleardomainconf PBLIN_API_KEY
_info "Adding TXT record for $fulldomain"
if ! _pblin_rest POST "domains/$_domain/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":600}"; then
_err "Failed to add TXT record: $_message"
return 1
fi
_info "TXT record added"
return 0
}
#Usage: dns_pblin_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_pblin_rm() {
fulldomain=$(_idn "$1")
txtvalue=$2
if ! _pblin_init; then
return 1
fi
if ! _get_root "$fulldomain"; then
return 1
fi
_info "Removing TXT record for $fulldomain"
if ! _pblin_rest GET "domains/$_domain/records?type=TXT&name=$fulldomain"; then
_err "Failed to list TXT records: $_message"
return 1
fi
#the API has no content filter and accepts duplicates, so match the value here and remove every copy
_record_ids="$(echo "$response" | tr '{' '\n' | grep "\"content\":\"$txtvalue\"" | _egrep_o '"id" *: *[0-9]*' | cut -d : -f 2 | tr -d ' ')"
_debug _record_ids "$_record_ids"
if [ -z "$_record_ids" ]; then
_info "TXT record already removed"
return 0
fi
for _record_id in $_record_ids; do
if ! _pblin_rest DELETE "domains/$_domain/records/$_record_id" && [ "$_code" != "404" ]; then
_err "Failed to remove TXT record $_record_id: $_message"
return 1
fi
done
_info "TXT record removed"
return 0
}
_pblin_init() {
#the SAVED_ prefix keeps a reloaded certificate conf from overriding a newly exported key
PBLIN_API_KEY="${PBLIN_API_KEY:-$(_readdomainconf SAVED_PBLIN_API_KEY)}"
#only renewals load the out-of-tree plugin's unprefixed key, so read it here for --issue too
PBLIN_API_KEY="${PBLIN_API_KEY:-$(_readdomainconf PBLIN_API_KEY)}"
if [ -z "$PBLIN_API_KEY" ]; then
_err "PBLIN_API_KEY is not set. Create an API key in the Public Internet customer portal at https://portal.publicinternet.com, then:"
_err "export PBLIN_API_KEY=\"your-api-key\""
return 1
fi
return 0
}
_get_root() {
domain=$1
i=1
while true; do
#-s: plain cut returns a dotless name (e.g. from --domain-alias) for every i, so the loop would keep querying it
h=$(printf "%s" "$domain" | cut -s -d . -f "$i"-100)
if [ -z "$h" ]; then
_err "No zone for $domain found in this Public Internet account"
return 1
fi
if _pblin_rest GET "domains/$h"; then
_domain=$h
return 0
fi
case "$_code" in
404) ;;
401 | 403)
_err "The API key was rejected ($_message), check PBLIN_API_KEY"
return 1
;;
*)
_err "Failed to look up zone $h: $_message"
return 1
;;
esac
i=$(_math "$i" + 1)
done
}
_pblin_rest() {
m=$1
ep=$2
data=$3
export _H1="Authorization: Bearer $PBLIN_API_KEY"
export _H2="Content-Type: application/json"
if [ "$m" = "GET" ]; then
response="$(_get "$PBLIN_Api/$ep")"
else
response="$(_post "$data" "$PBLIN_Api/$ep" "" "$m")"
fi
if [ "$?" != "0" ]; then
_code=""
_message="connection failed"
return 1
fi
#printf, not echo: dash's builtin echo interprets backslash escapes in record content
response="$(printf "%s\n" "$response" | _normalizeJson)"
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 _code "$_code"
_debug2 response "$response"
case "$_code" in
2*) return 0 ;;
esac
_message="$(echo "$response" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
_message="HTTP ${_code:-request failed}${_message:+: $_message}"
return 1
}
+34 -18
View File
@@ -114,21 +114,32 @@ _rm_record() {
full=$1 full=$1
txtvalue=$2 txtvalue=$2
if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records"; then if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records?type=TXT"; then
_err "Failed to retrieve records" _err "Failed to retrieve records"
return 1 return 1
fi fi
# The API returns: {"success":true,"data":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]} # The API returns: {"success":true,"data":{"records":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]},"message":"..."}
_txt_record_obj=$( _records="$(printf '%s\n' "$response" | tr -d '\r\n' | _egrep_o '\{[^{}]*\}')"
printf '%s\n' "$response" | _txt_record_obj=""
sed 's/^.*"data":\[//; s/\],"message":.*$//' | while IFS= read -r _rec || [ -n "$_rec" ]; do
awk '{ gsub(/},{/, "}\n{"); print }' | case "$_rec" in
grep -F "\"name\":\"$full\"" | *"\"name\":\"$full\""*) ;;
grep -F "\"type\":\"TXT\"" | *) continue ;;
grep -F "\"content\":\"$txtvalue\"" | esac
_head_n 1 case "$_rec" in
) *"\"type\":\"TXT\""*) ;;
*) continue ;;
esac
case "$_rec" in
*"\"content\":\"$txtvalue\""*)
_txt_record_obj="$_rec"
break
;;
esac
done <<EOF
$_records
EOF
if [ -z "$_txt_record_obj" ]; then if [ -z "$_txt_record_obj" ]; then
_info "TXT record not found for $full with content $txtvalue" _info "TXT record not found for $full with content $txtvalue"
@@ -173,7 +184,7 @@ _get_root() {
return 1 return 1
fi fi
_zones_response="$response" _zones="$(printf '%s\n' "$response" | tr -d '\r\n' | _egrep_o '\{[^{}]*\}')"
while true; do while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
@@ -183,12 +194,17 @@ _get_root() {
return 1 return 1
fi fi
zone_obj=$( zone_obj=""
printf '%s' "$_zones_response" | while IFS= read -r _zone || [ -n "$_zone" ]; do
sed 's/},{/}\n{/g' | case "$_zone" in
grep -F "\"name\":\"$h\"" | *"\"name\":\"$h\""*)
_head_n 1 zone_obj="$_zone"
) break
;;
esac
done <<EOF
$_zones
EOF
if [ -n "$zone_obj" ]; then if [ -n "$zone_obj" ]; then
_zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2) _zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2)
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_primedns_info='PrimeDNS
Site: primedns.net
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_primedns
Options:
PRIMEDNS_API_KEY API Key. Scope acme:name:_acme-challenge.<hostname> for each name you issue (or dns:write)
OptionsAlt:
PRIMEDNS_API_URL API base URL. Optional, default https://api.primedns.net/v1
Issues: github.com/acmesh-official/acme.sh/issues/7330
Author: PrimeDNS <acme@primedns.net>
'
PRIMEDNS_Api_Default="https://api.primedns.net/v1"
######## Public functions #####################
#Usage: dns_primedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_primedns_add() {
fulldomain=$(_idn "$1")
txtvalue=$2
PRIMEDNS_API_KEY="${PRIMEDNS_API_KEY:-$(_readaccountconf_mutable PRIMEDNS_API_KEY)}"
if [ -z "$PRIMEDNS_API_KEY" ]; then
PRIMEDNS_API_KEY=""
_err "You didn't specify a PrimeDNS API key (PRIMEDNS_API_KEY) yet."
_err "Create one in the PrimeDNS panel (Account -> API Keys) with the scope acme:name:_acme-challenge.<hostname> for each name you issue."
return 1
fi
PRIMEDNS_API_URL="${PRIMEDNS_API_URL:-$(_readaccountconf_mutable PRIMEDNS_API_URL)}"
#save the credentials to the account conf file.
_saveaccountconf_mutable PRIMEDNS_API_KEY "$PRIMEDNS_API_KEY"
if [ "$PRIMEDNS_API_URL" ]; then
_saveaccountconf_mutable PRIMEDNS_API_URL "$PRIMEDNS_API_URL"
fi
_info "Using PrimeDNS"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _primedns_rest POST "$fulldomain" "$txtvalue"; then
return 1
fi
_info "Added, OK"
return 0
}
#Usage: dns_primedns_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_primedns_rm() {
fulldomain=$(_idn "$1")
txtvalue=$2
PRIMEDNS_API_KEY="${PRIMEDNS_API_KEY:-$(_readaccountconf_mutable PRIMEDNS_API_KEY)}"
if [ -z "$PRIMEDNS_API_KEY" ]; then
PRIMEDNS_API_KEY=""
_err "You didn't specify a PrimeDNS API key (PRIMEDNS_API_KEY) yet."
return 1
fi
PRIMEDNS_API_URL="${PRIMEDNS_API_URL:-$(_readaccountconf_mutable PRIMEDNS_API_URL)}"
_info "Using PrimeDNS"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _primedns_rest DELETE "$fulldomain" "$txtvalue"; then
return 1
fi
_info "Removed, OK"
return 0
}
#################### Private functions below ##################################
#Usage: _primedns_rest POST|DELETE _acme-challenge.www.domain.com txtvalue
#The API creates/removes the TXT record with exactly the name given, so the full
#name acme.sh passes (_acme-challenge.<domain>, or the name of a DNS alias) is
#sent as it is.
_primedns_rest() {
_method=$1
_name=$2
_value=$3
_url="${PRIMEDNS_API_URL:-$PRIMEDNS_Api_Default}/dns/acme-challenge"
#The body is built by hand, so refuse anything that would need JSON escaping.
case "$_name$_value" in
*\"* | *\\*)
_err "Refusing to send '$_name': it contains a quote or a backslash."
return 1
;;
esac
if [ -z "$_name" ] || _is_idn "$_name"; then
_err "Cannot send '$_name': it is not a plain ASCII host name. For an IDN name install idn (acme.sh converts it to punycode) or pass the punycode name."
return 1
fi
_body="{\"name\":\"$_name\",\"value\":\"$_value\"}"
_debug _body "$_body"
_primedns_retried=""
while true; do
#acme.sh sends _H1.._H5 with every later request of this process, so the API key
#header must not stay set once this call is done (it would reach other hosts).
_primedns_old_h1="$_H1"
export _H1="X-API-Key: $PRIMEDNS_API_KEY"
: >"$HTTP_HEADER"
response="$(_post "$_body" "$_url" "" "$_method" "application/json")"
_ret=$?
export _H1="$_primedns_old_h1"
_status=$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
if [ "$_ret" != "0" ]; then
_err "Error calling the PrimeDNS API ($_method /dns/acme-challenge)."
return 1
fi
response="$(echo "$response" | _normalizeJson)"
_debug response "$response"
_debug _status "$_status"
#Rate limited or temporary server error: wait and try once more.
if [ -z "$_primedns_retried" ] && { [ "$_status" = "429" ] || [ "$_status" = "502" ] || [ "$_status" = "503" ] || [ "$_status" = "504" ]; }; then
_wait="$(_retryafter_seconds <"$HTTP_HEADER")"
case "$_wait" in
'' | *[!0-9]*) _wait=5 ;;
esac
[ "$_wait" -gt 60 ] && _wait=60
_info "PrimeDNS API answered HTTP $_status, retrying in $_wait seconds."
_sleep "$_wait"
_primedns_retried=1
continue
fi
break
done
if _contains "$response" '"success":true'; then
return 0
fi
if [ "$_status" = "401" ]; then
_err "The PrimeDNS API refused the key (HTTP 401): it is wrong, revoked or expired. Check PRIMEDNS_API_KEY."
elif _contains "$response" 'IP_NOT_ALLOWED'; then
_err "This PrimeDNS API key may not be used from this address (allowed_ips). Add the address to the key or run acme.sh from an allowed host."
elif _contains "$response" 'NAME_NOT_ALLOWED'; then
_err "This PrimeDNS API key may not write $_name: add the scope acme:name:$_name to the key, or the zone to its allowed zones."
elif _contains "$response" 'INSUFFICIENT_SCOPE'; then
_err "This PrimeDNS API key needs the scope acme:name:$_name (or dns:write)."
elif _contains "$response" 'DOMAIN_NOT_FOUND'; then
_err "$_name is not in a zone of the PrimeDNS account that owns this API key."
elif [ "$_status" = "429" ]; then
_err "PrimeDNS API rate limit still hit after a retry. Try again in a minute."
else
_err "PrimeDNS API error (HTTP $_status): $response"
fi
return 1
}
+123 -41
View File
@@ -4,14 +4,15 @@ dns_selfhost_info='SelfHost.de
Site: SelfHost.de Site: SelfHost.de
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_selfhost Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_selfhost
Options: Options:
SELFHOSTDNS_USERNAME Username SELFHOSTDNS_APIKEY API key
SELFHOSTDNS_PASSWORD Password
SELFHOSTDNS_MAP Subdomain name SELFHOSTDNS_MAP Subdomain name
SELFHOSTDNS_UPDATE_URL API url. Optional. Default "https://account.selfhost.de/cgi-bin/api.pl" SELFHOSTDNS_UPDATE_URL API url. Optional. Default "https://my.selfhost.de/cgi-bin/dns-api.pl"
Issues: github.com/acmesh-official/acme.sh/issues/4291 Issues: github.com/acmesh-official/acme.sh/issues/4291
Author: Marvin Edeler Author: Marvin Edeler
' '
DEFAULT_SELFHOSTDNS_UPDATE_URL="https://my.selfhost.de/cgi-bin/dns-api.pl"
dns_selfhost_add() { dns_selfhost_add() {
fulldomain=$1 fulldomain=$1
txt=$2 txt=$2
@@ -19,43 +20,26 @@ dns_selfhost_add() {
_debug fulldomain "$fulldomain" _debug fulldomain "$fulldomain"
_debug txtvalue "$txt" _debug txtvalue "$txt"
DEFAULT_SELFHOSTDNS_UPDATE_URL="https://account.selfhost.de/cgi-bin/api.pl" case "$fulldomain" in
acmetestXyzRandomName.*)
# The synthetic record of the DNS-API-Test, which expects add and
# rm to succeed. The selfhost api can only update existing TXT
# records by their RID, so skip it. Real records are never treated
# as a no-op.
_info "Skipping the DNS-API-Test record $fulldomain, the selfhost api can only update existing TXT records."
return 0
;;
esac
# Get values, but don't save until we successfully validated # Get values, but don't save until we successfully validated
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$(_readaccountconf_mutable SELFHOSTDNS_UPDATE_URL)}" if ! _selfhost_load_conf; then
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$DEFAULT_SELFHOSTDNS_UPDATE_URL}" return 1
SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}" fi
SELFHOSTDNS_PASSWORD="${SELFHOSTDNS_PASSWORD:-$(_readaccountconf_mutable SELFHOSTDNS_PASSWORD)}"
# These values are domain dependent, so read them from there
SELFHOSTDNS_MAP="${SELFHOSTDNS_MAP:-$(_readdomainconf SELFHOSTDNS_MAP)}"
# Selfhost api can't dynamically add TXT record, # Selfhost api can't dynamically add TXT record,
# so we have to store the last used RID of the domain to support a second RID for wildcard domains # so we have to store the last used RID of the domain to support a second RID for wildcard domains
# (format: 'fulldomainA:lastRid fulldomainB:lastRid ...') # (format: 'fulldomainA:lastRid fulldomainB:lastRid ...')
SELFHOSTDNS_MAP_LAST_USED_INTERNAL=$(_readdomainconf SELFHOSTDNS_MAP_LAST_USED_INTERNAL) SELFHOSTDNS_MAP_LAST_USED_INTERNAL=$(_readdomainconf SELFHOSTDNS_MAP_LAST_USED_INTERNAL)
if [ -z "${SELFHOSTDNS_USERNAME:-}" ] || [ -z "${SELFHOSTDNS_PASSWORD:-}" ]; then
_err "SELFHOSTDNS_USERNAME and SELFHOSTDNS_PASSWORD must be set"
return 1
fi
# get the domain entry from SELFHOSTDNS_MAP
# 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
# if the domain is defined multiple times only the last occurance will be matched
# 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"
if test -z "$mapEntry"; then
_err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID"
return 1
fi
# get the RIDs from the map entry
rid1=$(echo "$mapEntry" | cut -d: -f2)
rid2=$(echo "$mapEntry" | cut -d: -f3)
# read last used rid domain # read last used rid domain
lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*\).*/\1/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"
@@ -68,11 +52,7 @@ dns_selfhost_add() {
_info "Trying to add $txt on selfhost for rid: $rid" _info "Trying to add $txt on selfhost for rid: $rid"
data="?username=$SELFHOSTDNS_USERNAME&password=$SELFHOSTDNS_PASSWORD&rid=$rid&content=$txt" if ! _selfhost_api "present" "$rid" "$txt"; then
response="$(_get "$SELFHOSTDNS_UPDATE_URL$data")"
if ! echo "$response" | grep "200 OK" >/dev/null; then
_err "Invalid response of acme-dns for selfhost"
return 1 return 1
fi fi
@@ -96,11 +76,22 @@ dns_selfhost_add() {
fi fi
# Now that we know the values are good, save them # Now that we know the values are good, save them
_saveaccountconf_mutable SELFHOSTDNS_USERNAME "$SELFHOSTDNS_USERNAME" _saveaccountconf_mutable SELFHOSTDNS_APIKEY "$SELFHOSTDNS_APIKEY"
_saveaccountconf_mutable SELFHOSTDNS_PASSWORD "$SELFHOSTDNS_PASSWORD" # The old api credentials are not used anymore
_clearaccountconf_mutable SELFHOSTDNS_USERNAME
_clearaccountconf_mutable SELFHOSTDNS_PASSWORD
# These values are domain dependent, so store them there # These values are domain dependent, so store them there
_savedomainconf SELFHOSTDNS_MAP "$SELFHOSTDNS_MAP" _savedomainconf SELFHOSTDNS_MAP "$SELFHOSTDNS_MAP"
_savedomainconf SELFHOSTDNS_MAP_LAST_USED_INTERNAL "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" _savedomainconf SELFHOSTDNS_MAP_LAST_USED_INTERNAL "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL"
# remember which RID holds this txt value, so it can be cleaned up later
# (format: 'ridA:txtA ridB:txtB ...')
SELFHOSTDNS_TXT_RID_INTERNAL=$(_readdomainconf SELFHOSTDNS_TXT_RID_INTERNAL)
if test -z "$SELFHOSTDNS_TXT_RID_INTERNAL"; then
SELFHOSTDNS_TXT_RID_INTERNAL="$rid:$txt"
else
SELFHOSTDNS_TXT_RID_INTERNAL="$SELFHOSTDNS_TXT_RID_INTERNAL $rid:$txt"
fi
_savedomainconf SELFHOSTDNS_TXT_RID_INTERNAL "$SELFHOSTDNS_TXT_RID_INTERNAL"
} }
dns_selfhost_rm() { dns_selfhost_rm() {
@@ -108,5 +99,96 @@ dns_selfhost_rm() {
txt=$2 txt=$2
_debug fulldomain "$fulldomain" _debug fulldomain "$fulldomain"
_debug txtvalue "$txt" _debug txtvalue "$txt"
_info "Creating and removing of records is not supported by selfhost API, will not delete anything."
case "$fulldomain" in
acmetestXyzRandomName.*)
# See dns_selfhost_add.
_info "Skipping the DNS-API-Test record $fulldomain, the selfhost api can only update existing TXT records."
return 0
;;
esac
if ! _selfhost_load_conf; then
return 1
fi
# find the RID which was used for this txt value
SELFHOSTDNS_TXT_RID_INTERNAL=$(_readdomainconf SELFHOSTDNS_TXT_RID_INTERNAL)
rids=""
remainingTxtRidEntries=""
for txtRidEntry in $SELFHOSTDNS_TXT_RID_INTERNAL; do
if [ "${txtRidEntry#*:}" = "$txt" ]; then
rids="${txtRidEntry%%:*}"
elif test -z "$remainingTxtRidEntries"; then
remainingTxtRidEntries="$txtRidEntry"
else
remainingTxtRidEntries="$remainingTxtRidEntries $txtRidEntry"
fi
done
if test -z "$rids"; then
# unknown txt value, so try all RIDs of the domain
rids="$rid1 $rid2"
fi
for rid in $rids; do
_info "Trying to remove $txt on selfhost for rid: $rid"
if ! _selfhost_api "cleanup" "$rid" "$txt"; then
return 1
fi
done
_savedomainconf SELFHOSTDNS_TXT_RID_INTERNAL "$remainingTxtRidEntries"
}
#################### Private functions below ##################################
# reads the config and sets rid1 and rid2 for $fulldomain
_selfhost_load_conf() {
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$(_readaccountconf_mutable SELFHOSTDNS_UPDATE_URL)}"
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$DEFAULT_SELFHOSTDNS_UPDATE_URL}"
SELFHOSTDNS_APIKEY="${SELFHOSTDNS_APIKEY:-$(_readaccountconf_mutable SELFHOSTDNS_APIKEY)}"
# These values are domain dependent, so read them from there
SELFHOSTDNS_MAP="${SELFHOSTDNS_MAP:-$(_readdomainconf SELFHOSTDNS_MAP)}"
if [ -z "${SELFHOSTDNS_APIKEY:-}" ]; then
_err "SELFHOSTDNS_APIKEY must be set"
# hint for users still configured for the old username/password api
SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}"
SELFHOSTDNS_PASSWORD="${SELFHOSTDNS_PASSWORD:-$(_readaccountconf_mutable SELFHOSTDNS_PASSWORD)}"
if [ -n "${SELFHOSTDNS_USERNAME:-}" ] || [ -n "${SELFHOSTDNS_PASSWORD:-}" ]; then
_err "Selfhost has changed its API: SELFHOSTDNS_USERNAME and SELFHOSTDNS_PASSWORD are no longer supported, you have to migrate to SELFHOSTDNS_APIKEY."
_err "See https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_selfhost for instructions."
fi
return 1
fi
# get the domain entry from SELFHOSTDNS_MAP
# 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
# if the domain is defined multiple times only the last occurance will be matched
# 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"
if test -z "$mapEntry"; then
_err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID"
return 1
fi
# get the RIDs from the map entry
rid1=$(echo "$mapEntry" | cut -d: -f2)
rid2=$(echo "$mapEntry" | cut -d: -f3)
}
# usage: _selfhost_api action rid txt
_selfhost_api() {
data="{\"api_key\":\"$SELFHOSTDNS_APIKEY\",\"action\":\"$1\",\"record_id\":$2,\"content\":\"$3\"}"
response="$(_post "$data" "$SELFHOSTDNS_UPDATE_URL" "" "POST" "application/json")"
_debug2 response "$response"
if ! echo "$response" | grep '"ok" *: *1' >/dev/null; then
_err "Invalid response of acme-dns for selfhost: $response"
return 1
fi
} }
+13 -7
View File
@@ -106,14 +106,17 @@ dns_yc_add() {
_debug _domain "$_domain" _debug _domain "$_domain"
_debug "Getting txt records" _debug "Getting txt records"
if ! _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then if ! _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$fulldomain"; then
_err "Error: $response" _err "Error: $response"
return 1 return 1
fi fi
_info "Adding record" _info "Adding record"
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"merges\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then # The absolute name (trailing dot) also covers a record at the zone apex,
if _contains "$response" "\"done\": true"; then # e.g. with --domain-alias, which has no relative form: an empty name is
# rejected by the API.
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"merges\": [ { \"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
if _contains "$response" "\"done\": *true"; then
_info "Added, OK" _info "Added, OK"
return 0 return 0
else else
@@ -167,8 +170,8 @@ dns_yc_rm() {
# upsertRecordSets.deletions removes only the given value from the rrset, # upsertRecordSets.deletions removes only the given value from the rrset,
# leaving any other values at the same name (e.g. base + wildcard domain) # leaving any other values at the same name (e.g. base + wildcard domain)
# intact -- no need to read the current data set and recompute it. # intact -- no need to read the current data set and recompute it.
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
if _contains "$response" "\"done\": true"; then if _contains "$response" "\"done\": *true"; then
_info "Delete, OK" _info "Delete, OK"
return 0 return 0
else else
@@ -201,6 +204,8 @@ _get_root() {
if [ "$_domain" ]; then if [ "$_domain" ]; then
_cutlength=$((${#domain} - ${#_domain})) _cutlength=$((${#domain} - ${#_domain}))
_sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength") _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength")
# Strip the trailing dot: a relative name, as in the folder lookup below
_sub_domain="${_sub_domain%.}"
_domain_id=$YC_Zone_ID _domain_id=$YC_Zone_ID
return 0 return 0
else else
@@ -227,8 +232,9 @@ _get_root() {
echo "You didn't specify a Yandex Cloud Folder ID." echo "You didn't specify a Yandex Cloud Folder ID."
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 '"') # Drop labels objects first, their braces would break the [^{]* match
_domain_id=$(echo "$response" | _normalizeJson | sed "s/\"labels\":{[^}]*}/\"labels\":0/g" | _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")
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env sh
#Support Healthchecks.io (hosted or self-hosted)
#https://healthchecks.io/docs/http_api/
#Required:
#HEALTHCHECKS_URL="https://hc-ping.com/your-uuid"
#Use with --notify-level 3, so a ping is sent on every cron run, even when
#all certs are skipped. Otherwise Healthchecks reports the check as down.
healthchecks_send() {
_subject="$1"
_content="$2"
_statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
_debug "_subject" "$_subject"
_debug "_content" "$_content"
_debug "_statusCode" "$_statusCode"
HEALTHCHECKS_URL="${HEALTHCHECKS_URL:-$(_readaccountconf_mutable HEALTHCHECKS_URL)}"
if [ -z "$HEALTHCHECKS_URL" ]; then
HEALTHCHECKS_URL=""
_err "You didn't specify the Healthchecks.io ping url HEALTHCHECKS_URL yet."
_err "Example: export HEALTHCHECKS_URL=\"https://hc-ping.com/your-uuid\""
return 1
fi
_saveaccountconf_mutable HEALTHCHECKS_URL "$HEALTHCHECKS_URL"
_hc_url="${HEALTHCHECKS_URL%/}"
case "$_statusCode" in
0 | 2) ;;
1)
_hc_url="$_hc_url/fail"
;;
*)
_hc_url="$_hc_url/log"
;;
esac
_data="$_subject
$_content"
response="$(_post "$_data" "$_hc_url" "" "POST" "text/plain")"
if [ "$?" = "0" ] && [ "$response" = "OK" ]; then
_info "healthchecks ping success."
return 0
fi
_err "healthchecks ping error."
_err "$response"
return 1
}
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.