Compare commits

...

414 Commits
3.1.2 ... 3.1.3

Author SHA1 Message Date
neil
76d1377fc1 Merge pull request #6934 from acmesh-official/dev
sync
2026-04-28 22:22:24 +02:00
neil
15a0f52577 fix https://github.com/acmesh-official/acme.sh/issues/6930#issuecomment-4327834577 2026-04-28 09:09:50 +02:00
Adam Bodnar
fd6ff7b173 Add dns_cpanel_uapi DNS API plugin (#6878)
* Add dns_cpanel_uapi.sh
2026-04-26 14:29:40 +02:00
neil
ed6d1f3447 Merge pull request #6929 from acmesh-official/dev
sync
2026-04-24 23:30:42 +02:00
neil
0b2187ab3f add MidnightBSD 2026-04-24 22:43:25 +02:00
neil
4b8b23bb90 fix ca name 2026-04-24 21:47:27 +02:00
neil
d00b2722ee Merge pull request #6889 from achmadalifn4/dev
Add BytePlus ALB deployment script
2026-04-24 21:46:10 +02:00
neil
df49dd2ec9 Merge pull request #6928 from acmesh-official/dev
sync
2026-04-24 21:36:58 +02:00
Achmad Alif Nasrulloh
e9b0cafac5 Fix byteplus_alb.sh 2026-04-24 11:21:43 +07:00
Antoni Company
9882d534af fix: commit overhaul (#6915)
- Removed scope exclusion for "standard commit".
  - If 'device-and-networks' is excluded (previous behaviour), a certificate for Panorama (always outside of a template) will not be committed (imported to the config but never applied to Panorama). Therefore, panos.sh was only working for certificates used in templates and applied to devices, but not for the Panorama certificate itself.
  - According to the official documentation and the XML API Browser, there is no 'policy-and-objects' that can be excluded.
  - Although it is not mandatory that the user account is solely dedicated to replace certificates and to perform no other type of operations, it is recommended. If such recommendation is applied, the only changes being committed would be in relation to certificates. Therefore, it should be safe not to exclude any scopes.
- Changed the order for "force commit" from '<commit><partial><force>' (unofficial) to '<commit><force><partial>' (official). Both work, but it is recommended to use what is part of the official documentation and/or XML API Browser.
- Removed unofficial 'policy-and-objects' from commented out code (see above).
- Replaced 'exclude' with 'excluded' from commented out code, as per the official documentation. Both work, but see above.
- Replaced 'acmekeytest' with $_panos_user in the commented out code.

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

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

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

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

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

IPv6 addresses in URLs need to be written like this:

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

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

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

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

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

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

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

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

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

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

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

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

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

fix index of domain start

fix shfmt

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

This reverts commit 9fe4616664b897c9891271006e7489b10bb818ca.

fix 404 on dot ad hyphen

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

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

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

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

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

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

View File

@@ -66,7 +66,7 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Set env file
@@ -114,7 +114,7 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install tools
run: brew install socat
- name: Clone acmetest
@@ -165,7 +165,7 @@ jobs:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install cygwin base packages with chocolatey
run: |
choco config get cacheLocation
@@ -224,15 +224,16 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/freebsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg install -y socat curl
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@@ -251,7 +252,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
@@ -275,15 +280,16 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg_add socat curl libiconv
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@@ -302,7 +308,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
@@ -326,16 +336,17 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/netbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: |
/usr/sbin/pkg_add curl socat
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@@ -354,7 +365,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
@@ -378,16 +393,18 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/dragonflybsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: |
pkg install -y curl socat libnghttp2
pkg install -y libnghttp2
pkg install -y curl socat
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@@ -406,16 +423,76 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
MidnightBSD:
runs-on: ubuntu-latest
needs: DragonFlyBSD
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/midnightbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: mport install socat curl || true
usesh: true
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
Solaris:
runs-on: ubuntu-latest
needs: DragonFlyBSD
needs: MidnightBSD
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
@@ -434,14 +511,17 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/solaris-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
copyback: false
prepare: pkgutil -y -i socat
sync: nfs
prepare: |
pkgutil -U
pkgutil -y -i socat
run: |
pkg set-mediator -v -I default@1.1 openssl
export PATH=/usr/gnu/bin:$PATH
@@ -462,6 +542,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
Omnios:
@@ -485,13 +570,14 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/omnios-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
copyback: false
sync: nfs
prepare: pkg install socat
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
@@ -511,5 +597,127 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
OpenIndiana:
runs-on: ubuntu-latest
needs: Omnios
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since OpenIndiana doesn't accept the expired ISRG X1 root
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openindiana-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: pkg install socat
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
Haiku:
runs-on: ubuntu-latest
needs: OpenIndiana
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since OpenIndiana doesn't accept the expired ISRG X1 root
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/haiku-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: rsync
copyback: false
prepare: |
mkdir -p /boot/home/.cache
pkgman install -y cronie
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

View File

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

View File

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

84
.github/workflows/Haiku.yml vendored Normal file
View File

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

View File

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

View File

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

74
.github/workflows/MidnightBSD.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

80
.github/workflows/OpenIndiana.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,14 +43,14 @@ jobs:
if: "contains(needs.CheckToken.outputs.hasToken, 'true')"
steps:
- name: checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5.5.1
uses: docker/metadata-action@v6
with:
images: ${DOCKER_IMAGE}
- name: Set up Docker Buildx

View File

@@ -11,6 +11,7 @@ on:
jobs:
welcome:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- uses: actions/github-script@v6
with:

View File

@@ -13,6 +13,7 @@ on:
jobs:
welcome:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- uses: actions/github-script@v6
with:

View File

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

View File

@@ -6,9 +6,10 @@ on:
jobs:
notify:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- name: Checkout wiki repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
repository: ${{ github.repository }}.wiki
path: wiki
@@ -22,6 +23,7 @@ jobs:
page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH")
page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH")
page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH")
page_summary=$(jq -r '.pages[0].summary' "$GITHUB_EVENT_PATH")
now="$(date '+%Y-%m-%d %H:%M:%S')"
cd wiki
@@ -35,9 +37,11 @@ jobs:
{
echo "Wiki edited"
echo -n "User: "
echo "[$actor]($sender_url)"
echo "@$actor [$actor]($sender_url)"
echo "Time: $now"
echo "Page: [$page_name]($page_url) (Action: $page_action)"
echo "Comment: $page_summary"
echo "[Click here to Revert](${page_url}/_history)"
echo ""
echo "----"
echo "### diff"

View File

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

417
README.md
View File

@@ -1,54 +1,73 @@
[![zerossl.com](https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05)](https://zerossl.com/?fromacme.sh)
<p align="center">
<a href="https://zerossl.com/?fromacme.sh">
<img src="https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05" alt="zerossl.com">
</a>
</p>
# An ACME Shell script: acme.sh
<h1 align="center">🔐 acme.sh</h1>
<h3 align="center">An ACME Protocol Client Written Purely in Shell</h3>
[![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml)
[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)
[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)
[![MacOS](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml)
[![Ubuntu](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml)
[![Windows](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml)
[![Solaris](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml)
[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)
[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)
<p align="center">
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg" alt="FreeBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg" alt="OpenBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg" alt="NetBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg" alt="MacOS"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg" alt="Ubuntu"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg" alt="Windows"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg" alt="Solaris"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg" alt="DragonFlyBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml/badge.svg" alt="MidnightBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg" alt="Omnios"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg" alt="OpenIndiana"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg" alt="Haiku"></a>
</p>
![Shellcheck](https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg)
![PebbleStrict](https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg)
![DockerHub](https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg)
<p align="center">
<img src="https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg" alt="Shellcheck">
<img src="https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg" alt="PebbleStrict">
<img src="https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg" alt="DockerHub">
</p>
<p align="center">
<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/all/badge.svg?label=financial+contributors" alt="Financial Contributors on Open Collective"></a>
<a href="https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/acme-sh/Lobby.svg" alt="Join the chat at Gitter"></a>
<a href="https://hub.docker.com/r/neilpang/acme.sh" title="Click to view the image on Docker Hub"><img src="https://img.shields.io/docker/stars/neilpang/acme.sh.svg" alt="Docker stars"></a>
<a href="https://hub.docker.com/r/neilpang/acme.sh" title="Click to view the image on Docker Hub"><img src="https://img.shields.io/docker/pulls/neilpang/acme.sh.svg" alt="Docker pulls"></a>
</p>
<a href="https://opencollective.com/acmesh" alt="Financial Contributors on Open Collective"><img src="https://opencollective.com/acmesh/all/badge.svg?label=financial+contributors" /></a>
[![Join the chat at https://gitter.im/acme-sh/Lobby](https://badges.gitter.im/acme-sh/Lobby.svg)](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Docker stars](https://img.shields.io/docker/stars/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub")
[![Docker pulls](https://img.shields.io/docker/pulls/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub")
---
## ✨ Features
- 🐚 An ACME protocol client written **purely in Shell** (Unix shell) language
- 📜 Full ACME protocol implementation
- 🔑 Support **ECDSA** certificates
- 🌐 Support **SAN** and **wildcard** certificates
- ⚡ Simple, powerful and very easy to use — only **3 minutes** to learn!
- 🔧 Compatible with **Bash**, **dash** and **sh**
- 🚫 No dependencies on Python
- 🔄 One script to issue, renew and install your certificates automatically
- 👤 **DOES NOT** require `root/sudoer` access
- 🐳 Docker ready
- 🌍 IPv6 ready
- 📧 Cron job notifications for renewal or error
- An ACME protocol client written purely in Shell (Unix shell) language.
- Full ACME protocol implementation.
- Support ECDSA certs
- Support SAN and wildcard certs
- Simple, powerful and very easy to use. You only need 3 minutes to learn it.
- Bash, dash and sh compatible.
- Purely written in Shell with no dependencies on python.
- Just one script to issue, renew and install your certificates automatically.
- DOES NOT require `root/sudoer` access.
- Docker ready
- IPv6 ready
- Cron job notifications for renewal or error etc.
> 💡 It's probably the **easiest & smartest** shell script to automatically issue & renew free certificates.
It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates.
<p align="center">
<a href="https://github.com/acmesh-official/acme.sh/wiki"><strong>📚 Wiki</strong></a> •
<a href="https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker"><strong>🐳 Docker Guide</strong></a> •
<a href="https://twitter.com/neilpangxa"><strong>🐦 Twitter</strong></a>
</p>
Wiki: https://github.com/acmesh-official/acme.sh/wiki
---
For Docker Fans: [acme.sh :two_hearts: Docker ](https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker)
## 🌏 [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
---
# [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
# Who:
## 🏆 Who Uses acme.sh?
- [FreeBSD.org](https://blog.crashed.org/letsencrypt-in-freebsd-org/)
- [ruby-china.org](https://ruby-china.org/topics/31983)
- [Proxmox](https://pve.proxmox.com/wiki/Certificate_Management)
@@ -62,7 +81,9 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
- [lnmp.org](https://lnmp.org/)
- [more...](https://github.com/acmesh-official/acme.sh/wiki/Blogs-and-tutorials)
# Tested OS
---
## 🖥️ Tested OS
| NO | Status| Platform|
|----|-------|---------|
@@ -75,66 +96,78 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|7|[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)|OpenBSD
|8|[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)|NetBSD
|9|[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)|DragonFlyBSD
|10|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios
|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian
|12|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|CentOS
|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE
|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl)
|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux
|16|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora
|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux
|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux
|19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia
|10|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux
|10|[![MidnightBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml)|MidnightBSD
|11|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios
|12|[![OpenIndiana](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml)|OpenIndiana
|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian
|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE
|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl)
|16|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux
|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora
|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux
|19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux
|20|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia
|21|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux
|22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111
|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
|24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|25|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
Check our [testing project](https://github.com/acmesh-official/acmetest):
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
>
> 🖥️ The testing VMs are supported by [vmactions.org](https://vmactions.org)
https://github.com/acmesh-official/acmetest
---
# Supported CA
## 🏛️ Supported CA
- [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA)(default)
- Letsencrypt.org CA
- [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA)
- [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA)
- [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA)
- [Pebble strict Mode](https://github.com/letsencrypt/pebble)
- Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA
| CA | Status |
|---|---|
| [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA) | ⭐ **Default** |
| Letsencrypt.org CA | ✅ Supported |
| [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) | ✅ Supported |
| [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) | ✅ Supported |
| [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA) | ✅ Supported |
| [Pebble strict Mode](https://github.com/letsencrypt/pebble) | ✅ Supported |
| Any [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA | ✅ Supported |
# Supported modes
---
- Webroot mode
- Standalone mode
- Standalone tls-alpn mode
- Apache mode
- Nginx mode
- DNS mode
- [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode)
- [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode)
## ⚙️ Supported Modes
| Mode | Description |
|------|-------------|
| 📁 Webroot mode | Use existing webroot directory |
| 🖥️ Standalone mode | Built-in webserver on port 80 |
| 🔐 Standalone tls-alpn mode | Built-in webserver on port 443 |
| 🪶 Apache mode | Use Apache for verification |
| ⚡ Nginx mode | Use Nginx for verification |
| 🌐 DNS mode | Use DNS TXT records |
| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification |
| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification |
# 1. How to install
---
### 1. Install online
## 📖 Usage Guide
Check this project: https://github.com/acmesh-official/get.acme.sh
### 1⃣ How to Install
#### 📥 Install Online
> Check this project: https://github.com/acmesh-official/get.acme.sh
```bash
curl https://get.acme.sh | sh -s email=my@example.com
```
Or:
**Or:**
```bash
wget -O - https://get.acme.sh | sh -s email=my@example.com
```
### 2. Or, Install from git
#### 📦 Install from Git
Clone this project and launch installation:
@@ -144,11 +177,11 @@ cd ./acme.sh
./acme.sh --install -m my@example.com
```
You `don't have to be root` then, although `it is recommended`.
> 💡 You `don't have to be root` then, although `it is recommended`.
Advanced Installation: https://github.com/acmesh-official/acme.sh/wiki/How-to-install
📚 **Advanced Installation:** https://github.com/acmesh-official/acme.sh/wiki/How-to-install
The installer will perform 3 actions:
**The installer will perform 3 actions:**
1. Create and copy `acme.sh` to your home dir (`$HOME`): `~/.acme.sh/`.
All certs will be placed in this folder too.
@@ -161,17 +194,19 @@ Cron entry example:
0 0 * * * "/home/user/.acme.sh"/acme.sh --cron --home "/home/user/.acme.sh" > /dev/null
```
After the installation, you must close the current terminal and reopen it to make the alias take effect.
> ⚠️ After the installation, you must close the current terminal and reopen it to make the alias take effect.
Ok, you are ready to issue certs now.
**You are ready to issue certs now!**
Show help message:
**Show help message:**
```sh
root@v1:~# acme.sh -h
acme.sh -h
```
# 2. Just issue a cert
---
### 2⃣ Issue a Certificate
**Example 1:** Single domain.
@@ -206,19 +241,21 @@ You must point and bind all the domains to the same webroot dir: `/home/wwwroot/
The certs will be placed in `~/.acme.sh/example.com/`
The certs will be renewed automatically every **60** days.
> 🔄 The certs will be renewed automatically every **30** days.
The certs will default to ECC certificates.
> 🔐 The certs will default to **ECC** certificates.
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
---
# 3. Install the cert to Apache/Nginx etc.
### 3️⃣ Install the Certificate to Apache/Nginx
After the cert is generated, you probably want to install/copy the cert to your Apache/Nginx or other servers.
You **MUST** use this command to copy the certs to the target files, **DO NOT** use the certs files in **~/.acme.sh/** folder, they are for internal use only, the folder structure may change in the future.
**Apache** example:
> ⚠️ **IMPORTANT:** You **MUST** use this command to copy the certs to the target files. **DO NOT** use the certs files in `~/.acme.sh/` folder — they are for internal use only, the folder structure may change in the future.
#### 🪶 Apache Example:
```bash
acme.sh --install-cert -d example.com \
--cert-file /path/to/certfile/in/apache/cert.pem \
@@ -227,7 +264,7 @@ acme.sh --install-cert -d example.com \
--reloadcmd "service apache2 force-reload"
```
**Nginx** example:
#### ⚡ Nginx Example:
```bash
acme.sh --install-cert -d example.com \
--key-file /path/to/keyfile/in/nginx/key.pem \
@@ -241,91 +278,89 @@ The ownership and permission info of existing files are preserved. You can pre-c
Install/copy the cert/key to the production Apache or Nginx path.
The cert will be renewed every **60** days by default (which is configurable). Once the cert is renewed, the Apache/Nginx service will be reloaded automatically by the command: `service apache2 force-reload` or `service nginx force-reload`.
> 🔄 The cert will be renewed every **30** days by default (configurable). Once renewed, the Apache/Nginx service will be reloaded automatically.
> ⚠️ **IMPORTANT:** The `reloadcmd` is very important. The cert can be automatically renewed, but without a correct `reloadcmd`, the cert may not be flushed to your server (like nginx or apache), then your website will not be able to show the renewed cert.
**Please take care: The reloadcmd is very important. The cert can be automatically renewed, but, without a correct 'reloadcmd' the cert may not be flushed to your server(like nginx or apache), then your website will not be able to show renewed cert in 60 days.**
---
# 4. Use Standalone server to issue cert
### 4️⃣ Use Standalone Server to Issue Certificate
**(requires you to be root/sudoer or have permission to listen on port 80 (TCP))**
> 🔐 Requires root/sudoer or permission to listen on port **80** (TCP)
Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
> ⚠️ Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
```bash
acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com
```
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
# 5. Use Standalone ssl server to issue cert
---
**(requires you to be root/sudoer or have permission to listen on port 443 (TCP))**
### 5⃣ Use Standalone TLS Server to Issue Certificate
Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
> 🔐 Requires root/sudoer or permission to listen on port **443** (TCP)
> ⚠️ Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
```bash
acme.sh --issue --alpn -d example.com -d www.example.com -d cp.example.com
```
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
---
# 6. Use Apache mode
### 6️⃣ Use Apache Mode
**(requires you to be root/sudoer, since it is required to interact with Apache server)**
> 🔐 Requires root/sudoer to interact with Apache server
If you are running a web server, it is recommended to use the `Webroot mode`.
Particularly, if you are running an Apache server, you can use Apache mode instead. This mode doesn't write any files to your web root folder.
Just set string "apache" as the second argument and it will force use of apache plugin automatically.
```sh
acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com
```
**This apache mode is only to issue the cert, it will not change your apache config files.
You will need to configure your website config files to use the cert by yourself.
We don't want to mess with your apache server, don't worry.**
> 💡 **Note:** This Apache mode is only to issue the cert, it will **not** change your Apache config files. You will need to configure your website config files to use the cert by yourself. We don't want to mess with your Apache server, don't worry!
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
# 7. Use Nginx mode
---
**(requires you to be root/sudoer, since it is required to interact with Nginx server)**
### 7⃣ Use Nginx Mode
> 🔐 Requires root/sudoer to interact with Nginx server
If you are running a web server, it is recommended to use the `Webroot mode`.
Particularly, if you are running an nginx server, you can use nginx mode instead. This mode doesn't write any files to your web root folder.
Particularly, if you are running an Nginx server, you can use Nginx mode instead. This mode doesn't write any files to your web root folder.
Just set string "nginx" as the second argument.
It will configure nginx server automatically to verify the domain and then restore the nginx config to the original version.
So, the config is not changed.
It will configure Nginx server automatically to verify the domain and then restore the Nginx config to the original version. So, the config is not changed.
```sh
acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com
```
**This nginx mode is only to issue the cert, it will not change your nginx config files.
You will need to configure your website config files to use the cert by yourself.
We don't want to mess with your nginx server, don't worry.**
> 💡 **Note:** This Nginx mode is only to issue the cert, it will **not** change your Nginx config files. You will need to configure your website config files to use the cert by yourself. We don't want to mess with your Nginx server, don't worry!
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
# 8. Automatic DNS API integration
---
### 8⃣ Automatic DNS API Integration
If your DNS provider supports API access, we can use that API to automatically issue the certs.
You don't have to do anything manually!
> ✨ **You don't have to do anything manually!**
### Currently acme.sh supports most of the dns providers:
📚 **Currently acme.sh supports most DNS providers:** https://github.com/acmesh-official/acme.sh/wiki/dnsapi
https://github.com/acmesh-official/acme.sh/wiki/dnsapi
---
# 9. Use DNS manual mode:
### 9️⃣ Use DNS Manual Mode
See: https://github.com/acmesh-official/acme.sh/wiki/dns-manual-mode first.
@@ -355,67 +390,74 @@ Then just rerun with `renew` argument:
acme.sh --renew -d example.com
```
Ok, it's done.
**Done!**
**Take care, this is dns manual mode, it can not be renewed automatically. you will have to add a new txt record to your domain by your hand when you renew your cert.**
> ⚠️ **WARNING:** This is DNS manual mode it **cannot** be renewed automatically. You will have to add a new TXT record to your domain manually when you renew your cert. **Please use DNS API mode instead.**
**Please use dns api mode instead.**
---
# 10. Issue certificates of different key types and lengths (ECC or RSA)
### 🔟 Issue Certificates of Different Key Types (ECC or RSA)
Just set the `keylength` to a valid, supported, value.
Just set the `keylength` to a valid, supported value.
Valid values for the `keylength` parameter are:
**Valid values for the `keylength` parameter:**
1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)**
2. **ec-384 (secp384r1, "ECDSA P-384")**
3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)**
4. **2048 (RSA2048)**
5. **3072 (RSA3072)**
6. **4096 (RSA4096)**
| Key Length | Description |
|------------|-------------|
| `ec-256` | prime256v1, "ECDSA P-256" ⭐ **Default** |
| `ec-384` | secp384r1, "ECDSA P-384" |
| `ec-521` | secp521r1, "ECDSA P-521" ⚠️ Not supported by Let's Encrypt yet |
| `2048` | RSA 2048-bit |
| `3072` | RSA 3072-bit |
| `4096` | RSA 4096-bit |
For example:
**Examples:**
### Single domain with ECDSA P-384 certificate
#### Single domain with ECDSA P-384 certificate
```bash
acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-384
```
### SAN multi domain with RSA4096 certificate
#### SAN multi domain with RSA4096 certificate
```bash
acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096
```
# 11. Issue Wildcard certificates
---
It's simple, just give a wildcard domain as the `-d` parameter.
### 1⃣1⃣ Issue Wildcard Certificates
It's simple! Just give a wildcard domain as the `-d` parameter:
```sh
acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
```
# 12. How to renew the certs
---
No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days.
### 1⃣2⃣ How to Renew Certificates
However, you can also force to renew a cert:
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days.
However, you can force a renewal:
```sh
acme.sh --renew -d example.com --force
```
or, for ECC cert:
**For ECC cert:**
```sh
acme.sh --renew -d example.com --force --ecc
```
---
# 13. How to stop cert renewal
### 1⃣3 How to Stop Certificate Renewal
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
@@ -425,73 +467,78 @@ acme.sh --remove -d example.com [--ecc]
The cert/key file is not removed from the disk.
You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself.
> 💡 You can remove the respective directory (e.g. `~/.acme.sh/example.com`) manually.
---
# 14. How to upgrade `acme.sh`
### 1⃣4 How to Upgrade acme.sh
acme.sh is in constant development, so it's strongly recommended to use the latest code.
> 🚀 acme.sh is in constant development it's strongly recommended to use the latest code.
You can update acme.sh to the latest code:
**Update to latest:**
```sh
acme.sh --upgrade
```
You can also enable auto upgrade:
**Enable auto upgrade:**
```sh
acme.sh --upgrade --auto-upgrade
```
Then **acme.sh** will be kept up to date automatically.
Disable auto upgrade:
**Disable auto upgrade:**
```sh
acme.sh --upgrade --auto-upgrade 0
```
---
# 15. Issue a cert from an existing CSR
### 1⃣5 Issue a Certificate from an Existing CSR
https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
---
# 16. Send notifications in cronjob
### 1⃣6 Send Notifications in Cronjob
https://github.com/acmesh-official/acme.sh/wiki/notify
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
---
# 17. Under the Hood
### 1⃣7 Under the Hood
Speak ACME language using shell, directly to "Let's Encrypt".
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
TODO:
---
### 1⃣8⃣ Acknowledgments
# 18. Acknowledgments
| Project | Link |
|---------|------|
| 🙏 Acme-tiny | https://github.com/diafygi/acme-tiny |
| 📜 ACME protocol | https://github.com/ietf-wg-acme/acme |
1. Acme-tiny: https://github.com/diafygi/acme-tiny
2. ACME protocol: https://github.com/ietf-wg-acme/acme
---
## 👥 Contributors
## Contributors
### Code Contributors
### 💻 Code Contributors
This project exists thanks to all the people who contribute.
<a href="https://github.com/acmesh-official/acme.sh/graphs/contributors"><img src="https://opencollective.com/acmesh/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
### 💰 Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)]
#### Individuals
#### 👤 Individuals
<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/individuals.svg?width=890"></a>
#### Organizations
#### 🏢 Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/acmesh/contribute)]
@@ -506,25 +553,31 @@ Support this project with your organization. Your logo will show up here with a
<a href="https://opencollective.com/acmesh/organization/8/website"><img src="https://opencollective.com/acmesh/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/9/website"><img src="https://opencollective.com/acmesh/organization/9/avatar.svg"></a>
---
### 1⃣9⃣ License & Others
# 19. License & Others
📄 **License:** GPLv3
License is GPLv3
⭐ Please **Star** and **Fork** this project!
Please Star and Fork me.
🐛 [Issues](https://github.com/acmesh-official/acme.sh/issues) and 🔀 [Pull Requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome.
[Issues](https://github.com/acmesh-official/acme.sh/issues) and [pull requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome.
---
### 2⃣0⃣ Donate
# 20. Donate
Your donation makes **acme.sh** better:
> 💝 Your donation makes **acme.sh** better!
1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)
| Method | Link |
|--------|------|
| PayPal / Alipay(支付宝) / Wechat(微信) | [https://donate.acme.sh/](https://donate.acme.sh/) |
[Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)
📜 [Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)
# 21. About this repository
---
### 2⃣1⃣ About This Repository
> [!NOTE]
> This repository is officially maintained by <strong>ZeroSSL</strong> as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community!
@@ -532,7 +585,7 @@ Your donation makes **acme.sh** better:
>
> All donations made through this repository go directly to the original independent maintainer (Neil Pang), not to ZeroSSL.
<p align="center">
<a href="https://zerossl.com.com">
<a href="https://zerossl.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://zerossl.com/assets/images/zerossl_logo_white.svg">
<source media="(prefers-color-scheme: light)" srcset="https://zerossl.com/assets/images/zerossl_logo.svg">

238
acme.sh
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env sh
VER=3.1.2
VER=3.1.3
PROJECT_NAME="acme.sh"
@@ -65,7 +65,7 @@ ID_TYPE_IP="ip"
LOCAL_ANY_ADDRESS="0.0.0.0"
DEFAULT_RENEW=60
DEFAULT_RENEW="${DEFAULT_RENEW:-30}"
NO_VALUE="no"
@@ -250,6 +250,13 @@ _dlg_versions() {
socat -V 2>&1
else
_debug "socat doesn't exist."
if _exists "python3"; then
python3 -V 2>&1
elif _exists "python2"; then
python2 -V 2>&1
elif _exists "python"; then
python -V 2>&1
fi
fi
}
@@ -588,11 +595,6 @@ if [ "$(printf '\x41')" != 'A' ]; then
_URGLY_PRINTF=1
fi
_ESCAPE_XARGS=""
if _exists xargs && [ "$(printf %s '\\x41' | xargs printf)" = 'A' ]; then
_ESCAPE_XARGS=1
fi
_h2b() {
if _exists xxd; then
if _contains "$(xxd --help 2>&1)" "assumes -c30"; then
@@ -611,17 +613,8 @@ _h2b() {
jc=""
_debug2 _URGLY_PRINTF "$_URGLY_PRINTF"
if [ -z "$_URGLY_PRINTF" ]; then
if [ "$_ESCAPE_XARGS" ] && _exists xargs; then
_debug2 "xargs"
echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/g' | xargs printf
else
for h in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/ \1/g'); do
if [ -z "$h" ]; then
break
fi
printf "\x$h%s"
done
fi
# shellcheck disable=SC2059
printf "$(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/\\x\1/g')"
else
for c in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\)/ \1/g'); do
if [ -z "$ic" ]; then
@@ -1031,7 +1024,7 @@ _digest() {
outputhex="$2"
if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
if [ "$alg" = "sha3-256" ] || [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
if [ "$outputhex" ]; then
${ACME_OPENSSL_BIN:-openssl} dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' '
else
@@ -1466,7 +1459,7 @@ _toPkcs() {
${ACME_OPENSSL_BIN:-openssl} pkcs12 -export -out "$_cpfx" -inkey "$_ckey" -in "$_ccert" -certfile "$_cca"
fi
if [ "$?" = "0" ]; then
_savedomainconf "Le_PFXPassword" "$pfxPassword"
_savedomainconf "Le_PFXPassword" "$pfxPassword" "base64"
fi
}
@@ -1606,6 +1599,7 @@ createCSR() {
domain="$1"
domainlist="$2"
_isEcc="$3"
_csreku="$4"
_initpath "$domain" "$_isEcc"
@@ -1619,7 +1613,7 @@ createCSR() {
_err "Please create it first."
return 1
fi
_createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"
_createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_csreku"
}
@@ -2351,6 +2345,7 @@ _setopt() {
fi
if [ ! -f "$__conf" ]; then
touch "$__conf"
chmod 600 "$__conf"
fi
if [ -n "$(_tail_c 1 <"$__conf")" ]; then
echo >>"$__conf"
@@ -2559,41 +2554,76 @@ _startserver() {
_debug Le_Listen_V4 "$Le_Listen_V4"
_debug Le_Listen_V6 "$Le_Listen_V6"
_NC="socat"
if [ "$Le_Listen_V6" ]; then
_NC="$_NC -6"
SOCAT_OPTIONS=TCP6-LISTEN
elif [ "$Le_Listen_V4" ]; then
_NC="$_NC -4"
SOCAT_OPTIONS=TCP4-LISTEN
else
SOCAT_OPTIONS=TCP-LISTEN
fi
if _exists "socat"; then
_NC="socat"
if [ "$Le_Listen_V6" ]; then
_NC="$_NC -6"
SOCAT_OPTIONS=TCP6-LISTEN
elif [ "$Le_Listen_V4" ]; then
_NC="$_NC -4"
SOCAT_OPTIONS=TCP4-LISTEN
else
SOCAT_OPTIONS=TCP-LISTEN
fi
if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then
_NC="$_NC -d -d -v"
fi
if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then
_NC="$_NC -d -d -v"
fi
SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork
SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork
#Adding bind to local-address
if [ "$ncaddr" ]; then
SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}"
fi
#Adding bind to local-address
if [ "$ncaddr" ]; then
SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}"
fi
_content_len="$(printf "%s" "$content" | wc -c)"
_debug _content_len "$_content_len"
_debug "_NC" "$_NC $SOCAT_OPTIONS"
export _SOCAT_ERR="$(_mktemp)"
$_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \
_content_len="$(printf "%s" "$content" | wc -c)"
_debug _content_len "$_content_len"
_debug "_NC" "$_NC $SOCAT_OPTIONS"
export _SOCAT_ERR="$(_mktemp)"
$_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \
echo 'HTTP/1.0 200 OK'; \
echo 'Content-Length\: $_content_len'; \
echo ''; \
printf '%s' '$content';" 2>"$_SOCAT_ERR" &
serverproc="$!"
serverproc="$!"
else
_PYTHON=""
if _exists "python3"; then
_PYTHON="python3"
elif _exists "python2"; then
_PYTHON="python2"
elif _exists "python"; then
_PYTHON="python"
fi
if [ "$_PYTHON" ]; then
_debug "Using python: $_PYTHON"
_AF="socket.AF_INET"
_BIND_ADDR="0.0.0.0"
if [ "$Le_Listen_V6" ]; then
_AF="socket.AF_INET6"
_BIND_ADDR="::"
fi
if [ "$ncaddr" ]; then
_BIND_ADDR="$ncaddr"
fi
export _SOCAT_ERR="$(_mktemp)"
$_PYTHON -c "import socket,sys;s=socket.socket($_AF,socket.SOCK_STREAM);s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind((sys.argv[2],int(sys.argv[1])));s.listen(5);res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3];
while True:
c,a=s.accept()
c.sendall(res.encode() if hasattr(res, 'encode') else res)
c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" &
serverproc="$!"
_NC="$_PYTHON"
else
_err "Please install socat or python first for standalone mode."
return 1
fi
fi
if [ -f "$_SOCAT_ERR" ]; then
if grep "Permission denied" "$_SOCAT_ERR" >/dev/null; then
_err "socat: $(cat $_SOCAT_ERR)"
_err "$_NC: $(cat $_SOCAT_ERR)"
_err "Can not listen for user: $(whoami)"
_err "Maybe try with root again?"
rm -f "$_SOCAT_ERR"
@@ -2783,6 +2813,7 @@ _clearAPI() {
ACME_REVOKE_CERT=""
ACME_NEW_NONCE=""
ACME_AGREEMENT=""
ACME_RENEWAL_INFO=""
}
#server
@@ -2827,6 +2858,9 @@ _initAPI() {
ACME_AGREEMENT=$(echo "$response" | _egrep_o 'termsOfService" *: *"[^"]*"' | cut -d '"' -f 3)
export ACME_AGREEMENT
ACME_RENEWAL_INFO=$(echo "$response" | _egrep_o 'renewalInfo" *: *"[^"]*"' | cut -d '"' -f 3)
export ACME_RENEWAL_INFO
_debug "ACME_KEY_CHANGE" "$ACME_KEY_CHANGE"
_debug "ACME_NEW_AUTHZ" "$ACME_NEW_AUTHZ"
_debug "ACME_NEW_ORDER" "$ACME_NEW_ORDER"
@@ -2834,6 +2868,7 @@ _initAPI() {
_debug "ACME_REVOKE_CERT" "$ACME_REVOKE_CERT"
_debug "ACME_AGREEMENT" "$ACME_AGREEMENT"
_debug "ACME_NEW_NONCE" "$ACME_NEW_NONCE"
_debug "ACME_RENEWAL_INFO" "$ACME_RENEWAL_INFO"
if [ "$ACME_NEW_ACCOUNT" ] && [ "$ACME_NEW_ORDER" ]; then
return 0
fi
@@ -3552,9 +3587,9 @@ _on_before_issue() {
fi
fi
if _hasfield "$_chk_web_roots" "$NO_VALUE"; then
if ! _exists "socat"; then
_err "Please install socat tools first."
if _hasfield "$_chk_web_roots" "$NO_VALUE" && [ "$_chk_web_roots" = "$NO_VALUE" ]; then
if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then
_err "Please install socat or python tools first."
return 1
fi
fi
@@ -4465,7 +4500,7 @@ issue() {
Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime)
_debug Le_NextRenewTime "$Le_NextRenewTime"
if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
_valid_to_saved=$(_readdomainconf Le_Valid_to)
_valid_to_saved=$(_readdomainconf Le_Valid_To)
if [ "$_valid_to_saved" ] && ! _startswith "$_valid_to_saved" "+"; then
_info "The domain is set to be valid to: $_valid_to_saved"
_info "It cannot be renewed automatically"
@@ -5147,7 +5182,12 @@ $_authorizations_map"
if [ "$DEBUG" ]; then
if [ "$vtype" = "$VTYPE_HTTP" ]; then
_debug "Debug: GET token URL."
_get "http://$d/.well-known/acme-challenge/$token" "" 1
if _isIPv6 "$d"; then
host="[$d]"
else
host="$d"
fi
_get "http://$host/.well-known/acme-challenge/$token" "" 1
fi
fi
_clearupwebbroot "$_currentRoot" "$removelevel" "$token"
@@ -5246,7 +5286,7 @@ $_authorizations_map"
_info "Order status is 'ready', let's sleep and retry."
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
_debug "_retryafter" "$_retryafter"
if [ "$_retryafter" ]; then
if [ "$_retryafter" ] && [ $_retryafter -gt 0 ]; then
_info "Sleeping for $_retryafter seconds then retrying"
_sleep $_retryafter
else
@@ -5256,7 +5296,7 @@ $_authorizations_map"
_info "Order status is 'processing', let's sleep and retry."
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
_debug "_retryafter" "$_retryafter"
if [ "$_retryafter" ]; then
if [ "$_retryafter" ] && [ $_retryafter -gt 0 ]; then
_info "Sleeping for $_retryafter seconds then retrying"
_sleep $_retryafter
else
@@ -5285,6 +5325,11 @@ $_authorizations_map"
_link_cert_retry="$(_math $_link_cert_retry + 1)"
done
# cover case where the final poll returned 'valid'
if [ -z "$Le_LinkCert" ] && _contains "$response" "\"status\":\"valid\""; then
Le_LinkCert="$(echo "$response" | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)"
fi
if [ -z "$Le_LinkCert" ]; then
_err "Signing failed. Could not get Le_LinkCert, and stopped retrying after reaching the retry limit."
_err "$response"
@@ -5450,10 +5495,10 @@ $_authorizations_map"
_savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
#convert to pkcs12
Le_PFXPassword="$(_readdomainconf Le_PFXPassword)"
if [ "$Le_PFXPassword" ]; then
_toPkcs "$CERT_PFX_PATH" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$Le_PFXPassword"
fi
export CERT_PFX_PATH
if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then
_savedomainconf "Le_RealCertPath" "$_real_cert"
@@ -5516,16 +5561,17 @@ renew() {
. "$DOMAIN_CONF"
_debug Le_API "$Le_API"
case "$Le_API" in
"$CA_LETSENCRYPT_V2_TEST")
_info "Switching back to $CA_LETSENCRYPT_V2"
Le_API="$CA_LETSENCRYPT_V2"
;;
"$CA_GOOGLE_TEST")
_info "Switching back to $CA_GOOGLE"
Le_API="$CA_GOOGLE"
;;
esac
#don't switch it back
# case "$Le_API" in
# "$CA_LETSENCRYPT_V2_TEST")
# _info "Switching back to $CA_LETSENCRYPT_V2"
# Le_API="$CA_LETSENCRYPT_V2"
# ;;
# "$CA_GOOGLE_TEST")
# _info "Switching back to $CA_GOOGLE"
# Le_API="$CA_GOOGLE"
# ;;
# esac
if [ "$_server" ]; then
Le_API="$_server"
@@ -5563,6 +5609,10 @@ renew() {
Le_RenewHook="$(_readdomainconf Le_RenewHook)"
Le_Preferred_Chain="$(_readdomainconf Le_Preferred_Chain)"
Le_Certificate_Profile="$(_readdomainconf Le_Certificate_Profile)"
Le_Valid_From="$(_readdomainconf Le_Valid_From)"
Le_Valid_To="$(_readdomainconf Le_Valid_To)"
Le_ExtKeyUse="$(_readdomainconf Le_ExtKeyUse)"
# When renewing from an old version, the empty Le_Keylength means 2048.
# Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over
# time but an empty value implies 2048 specifically.
@@ -5623,7 +5673,7 @@ renewAll() {
_set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT}
_debug "_set_level" "$_set_level"
export _ACME_IN_RENEWALL=1
for di in "${CERT_HOME}"/*.*/; do
for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do
_debug di "$di"
if ! [ -d "$di" ]; then
_debug "Not a directory, skipping: $di"
@@ -5721,6 +5771,9 @@ ${_skipped_msg}
fi
fi
if [ "$_TREAT_SKIP_AS_SUCCESS" ] && [ "$_ret" = "$RENEW_SKIP" ]; then
_ret=0
fi
return "$_ret"
}
@@ -5744,6 +5797,10 @@ signcsr() {
_local_addr="${11}"
_challenge_alias="${12}"
_preferred_chain="${13}"
_valid_f="${14}"
_valid_t="${15}"
_cert_prof="${16}"
_en_key_usage="${17}"
_csrsubj=$(_readSubjectFromCSR "$_csrfile")
if [ "$?" != "0" ]; then
@@ -5787,7 +5844,7 @@ signcsr() {
_info "Copying CSR to: $CSR_PATH"
cp "$_csrfile" "$CSR_PATH"
issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain"
issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain" "$_valid_f" "$_valid_t" "$_cert_prof" "$_en_key_usage"
}
@@ -5840,7 +5897,8 @@ list() {
if [ -z "$_domain" ]; then
printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Profile${_sep}CA${_sep}Created${_sep}Renew"
fi
for di in "${CERT_HOME}"/*.*/; do
for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do
[ -d "$di" ] || continue
d=$(basename "$di")
_debug d "$d"
(
@@ -6537,6 +6595,36 @@ deactivate() {
done
}
#cert
_getAKI() {
_cert="$1"
openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ' :'
}
#cert
_getSerial() {
_cert="$1"
openssl x509 -in "$_cert" -serial -noout | cut -d = -f 2
}
#cert
_get_ARI() {
_cert="$1"
_aki=$(_getAKI "$_cert")
_ser=$(_getSerial "$_cert")
_debug2 "_aki" "$_aki"
_debug2 "_ser" "$_ser"
_akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)"
_debug2 "_akiurl" "$_akiurl"
_serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)"
_debug2 "_serurl" "$_serurl"
_ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl"
_get "$_ARI_URL"
}
# Detect profile file if not specified as environment variable
_detect_profile() {
if [ -n "$PROFILE" -a -f "$PROFILE" ]; then
@@ -6585,6 +6673,7 @@ _initconf() {
#NO_TIMESTAMP=1
" >"$ACCOUNT_CONF_PATH"
chmod 600 "$ACCOUNT_CONF_PATH"
fi
}
@@ -6620,9 +6709,9 @@ _precheck() {
return 1
fi
if ! _exists "socat"; then
_err "It is recommended to install socat first."
_err "We use socat for the standalone server, which is used for standalone mode."
if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then
_err "It is recommended to install socat or python first."
_err "We use socat or python for the standalone server, which is used for standalone mode."
_err "If you don't want to use standalone mode, you may ignore this warning."
fi
@@ -6903,6 +6992,7 @@ cron() {
_info "Automatically upgraded to: $VER"
fi
_TREAT_SKIP_AS_SUCCESS="1"
renewAll
_ret="$?"
_ACME_IN_CRON=""
@@ -7150,6 +7240,7 @@ Parameters:
--local-address <ip> Specifies the standalone/tls server listening address, in case you have multiple ip addresses.
--listraw Only used for '--list' command, list the certs in raw format.
-se, --stop-renew-on-error Only valid for '--renew-all' command. Stop if one cert has error in renewal.
--treat-skip-as-success Only valid for '--renew-all' command. Treat skipped certs as success, return 0 instead of $RENEW_SKIP.
--insecure Do not check the server certificate, in some devices, the api server's certificate may not be trusted.
--ca-bundle <file> Specifies the path to the CA certificate bundle to verify api server's certificate.
--ca-path <directory> Specifies directory containing CA certificates in PEM format, used by wget or curl.
@@ -7630,6 +7721,9 @@ _process() {
-f | --force)
FORCE="1"
;;
--treat-skip-as-success | --treatskipassuccess)
_TREAT_SKIP_AS_SUCCESS="1"
;;
--staging | --test)
STAGE="1"
;;
@@ -8112,7 +8206,7 @@ _process() {
deploy "$_domain" "$_deploy_hook" "$_ecc"
;;
signcsr)
signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain"
signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage"
;;
showcsr)
showcsr "$_csr" "$_domain"
@@ -8166,7 +8260,7 @@ _process() {
createDomainKey "$_domain" "$_keylength"
;;
createCSR)
createCSR "$_domain" "$_altdomains" "$_ecc"
createCSR "$_domain" "$_altdomains" "$_ecc" "$_extended_key_usage"
;;
setnotify)
setnotify "$_notify_hook" "$_notify_level" "$_notify_mode" "$_notify_source"

View File

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

View File

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

394
deploy/byteplus_alb.sh Normal file
View File

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

View File

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

157
deploy/localcopy.sh Normal file
View File

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

276
deploy/multideploy.sh Normal file
View File

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

View File

@@ -68,8 +68,8 @@ deployer() {
# Get Version Info to test key
content="type=version&key=$_panos_key"
## Exclude all scopes for the empty commit
#_exclude_scope="<policy-and-objects>exclude</policy-and-objects><device-and-network>exclude</device-and-network><shared-object>exclude</shared-object>"
#content="type=commit&action=partial&key=$_panos_key&cmd=<commit><partial>$_exclude_scope<admin><member>acmekeytest</member></admin></partial></commit>"
#_exclude_scope="<device-and-network>excluded</device-and-network><shared-object>excluded</shared-object>"
#content="type=commit&action=partial&key=$_panos_key&cmd=<commit><partial>$_exclude_scope<admin><member>$_panos_user</member></admin></partial></commit>"
fi
# Generate API Key
@@ -128,10 +128,9 @@ deployer() {
#Check for force commit - will commit ALL uncommited changes to the firewall. Use with caution!
if [ "$FORCE" ]; then
_debug "Force switch detected. Committing ALL changes to the firewall."
cmd=$(printf "%s" "<commit><partial><force><admin><member>$_panos_user</member></admin></force></partial></commit>" | _url_encode)
cmd=$(printf "%s" "<commit><force><partial><admin><member>$_panos_user</member></admin></partial></force></commit>" | _url_encode)
else
_exclude_scope="<policy-and-objects>exclude</policy-and-objects><device-and-network>exclude</device-and-network>"
cmd=$(printf "%s" "<commit><partial>$_exclude_scope<admin><member>$_panos_user</member></admin></partial></commit>" | _url_encode)
cmd=$(printf "%s" "<commit><partial><admin><member>$_panos_user</member></admin></partial></commit>" | _url_encode)
fi
content="type=commit&action=partial&key=$_panos_key&cmd=$cmd"
fi
@@ -207,13 +206,12 @@ panos_deploy() {
fi
# PANOS_KEY
_getdeployconf PANOS_KEY
if [ "$PANOS_KEY" ]; then
_debug "Detected saved key."
_panos_key=$PANOS_KEY
_debug "Detected ENV variable PANOS_KEY. Saving to file."
_savedeployconf PANOS_KEY "$PANOS_KEY" 1
else
_debug "No key detected"
unset _panos_key
_debug "Attempting to load variable PANOS_KEY from file."
_getdeployconf PANOS_KEY
fi
# PANOS_TEMPLATE
@@ -256,6 +254,7 @@ panos_deploy() {
_panos_host=$PANOS_HOST
_panos_user=$PANOS_USER
_panos_pass=$PANOS_PASS
_panos_key=$PANOS_KEY
_panos_template=$PANOS_TEMPLATE
_panos_template_stack=$PANOS_TEMPLATE_STACK
_panos_vsys=$PANOS_VSYS
@@ -271,12 +270,6 @@ panos_deploy() {
if [ -z "$_panos_host" ]; then
_err "No host found. If this is your first time deploying, please set PANOS_HOST in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
elif [ -z "$_panos_user" ]; then
_err "No user found. If this is your first time deploying, please set PANOS_USER in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
elif [ -z "$_panos_pass" ]; then
_err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
else
# Use certificate name based on the first domain on the certificate if no custom certificate name is set
if [ -z "$_panos_certname" ]; then
@@ -286,6 +279,13 @@ panos_deploy() {
# Generate a new API key if no valid API key is found
if [ -z "$_panos_key" ]; then
if [ -z "$_panos_user" ]; then
_err "No user found. If this is your first time deploying, please set PANOS_USER in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
elif [ -z "$_panos_pass" ]; then
_err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
fi
_debug "**** Generating new PANOS API KEY ****"
deployer keygen
_savedeployconf PANOS_KEY "$_panos_key" 1

View File

@@ -116,13 +116,15 @@ HEREDOC
export HTTPS_INSECURE=1
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
_retval=$?
if [ "${_retval}" -eq 0 ]; then
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
else
_err "Certificate deployment failed"
_err "Certificate deployment failed: $message"
_debug "Response" "$response"
return 1
fi

View File

@@ -128,13 +128,15 @@ HEREDOC
export HTTPS_INSECURE=1
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
_retval=$?
if [ "${_retval}" -eq 0 ]; then
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
else
_err "Certificate deployment failed"
_err "Certificate deployment failed: $message"
_debug "Response" "$response"
return 1
fi

View File

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

View File

@@ -238,6 +238,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
return $_err_code
fi
else
# If file doesn't exist, create it and change its permissions.
_cmdstr="$_cmdstr test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE;"
# ssh echo to the file
_cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;"
_info "will copy private key to remote file $DEPLOY_SSH_KEYFILE"

View File

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

View File

@@ -353,7 +353,7 @@ synology_dsm_deploy() {
_debug2 SYNO_CREATE "$SYNO_CREATE"
if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then
_err "Unable to find certificate: $SYNO_CERTIFICATE and $SYNO_CREATE is not set."
_err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set."
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
@@ -387,7 +387,7 @@ synology_dsm_deploy() {
if echo "$response" | grep '"restart_httpd":true' >/dev/null; then
_info "Restart HTTP services succeeded."
else
_info "Restart HTTP services failed."
_info "Restart HTTP services not necessary."
fi
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_logout

View File

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

View File

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

548
dnsapi/dns_baidu.sh Normal file
View File

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

202
dnsapi/dns_bh.sh Executable file
View File

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

373
dnsapi/dns_bhosted.sh Normal file
View File

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

269
dnsapi/dns_cpanel_uapi.sh Executable file
View File

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

View File

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

201
dnsapi/dns_czechia.sh Normal file
View File

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

View File

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

139
dnsapi/dns_efficientip.sh Executable file
View File

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

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

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

View File

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

303
dnsapi/dns_gname.sh Normal file
View File

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

501
dnsapi/dns_hostup.sh Normal file
View File

@@ -0,0 +1,501 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034,SC2154
dns_hostup_info='HostUp DNS
Site: hostup.se
Docs: https://developer.hostup.se/
Options:
HOSTUP_API_KEY Required. HostUp API key with read:dns + write:dns + read:domains scopes.
HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api).
HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds).
HOSTUP_ZONE_ID Optional. Force a specific zone ID (skip auto-detection).
Author: HostUp (https://cloud.hostup.se/contact/en)
'
HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api"
HOSTUP_DEFAULT_TTL=60
# Public: add TXT record
# Usage: dns_hostup_add _acme-challenge.example.com "txt-value"
dns_hostup_add() {
fulldomain="$1"
txtvalue="$2"
_info "Using HostUp DNS API"
if ! _hostup_init; then
return 1
fi
if ! _hostup_detect_zone "$fulldomain"; then
_err "Unable to determine HostUp zone for $fulldomain"
return 1
fi
record_name="$(_hostup_record_name "$fulldomain" "$HOSTUP_ZONE_DOMAIN")"
record_name="$(_hostup_sanitize_name "$record_name")"
record_value="$(_hostup_json_escape "$txtvalue")"
ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}"
_debug "zone_id" "$HOSTUP_ZONE_ID"
_debug "zone_domain" "$HOSTUP_ZONE_DOMAIN"
_debug "record_name" "$record_name"
_debug "ttl" "$ttl"
request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$record_value\",\"ttl\":$ttl}"
if ! _hostup_rest "POST" "/dns/zones/$HOSTUP_ZONE_ID/records" "$request_body"; then
return 1
fi
if ! _contains "$_hostup_response" '"success":true'; then
_err "HostUp DNS API: failed to create TXT record for $fulldomain"
_debug2 "_hostup_response" "$_hostup_response"
return 1
fi
record_id="$(_hostup_extract_record_id "$_hostup_response")"
if [ -n "$record_id" ]; then
_hostup_save_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_id"
_debug "hostup_saved_record_id" "$record_id"
fi
_info "Added TXT record for $fulldomain"
return 0
}
# Public: remove TXT record
# Usage: dns_hostup_rm _acme-challenge.example.com "txt-value"
dns_hostup_rm() {
fulldomain="$1"
txtvalue="$2"
_info "Using HostUp DNS API"
if ! _hostup_init; then
return 1
fi
if ! _hostup_detect_zone "$fulldomain"; then
_err "Unable to determine HostUp zone for $fulldomain"
return 1
fi
record_name_fqdn="$(_hostup_fqdn "$fulldomain")"
record_value="$txtvalue"
record_id_cached="$(_hostup_get_saved_record_id "$HOSTUP_ZONE_ID" "$fulldomain")"
if [ -n "$record_id_cached" ]; then
_debug "hostup_record_id_cached" "$record_id_cached"
if _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$record_id_cached"; then
_info "Deleted TXT record $record_id_cached"
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
HOSTUP_ZONE_ID=""
return 0
fi
fi
if ! _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$record_value"; then
_info "TXT record not found for $record_name_fqdn. Skipping removal."
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
return 0
fi
_debug "Deleting record" "$HOSTUP_RECORD_ID"
if ! _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$HOSTUP_RECORD_ID"; then
return 1
fi
_info "Deleted TXT record $HOSTUP_RECORD_ID"
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
HOSTUP_ZONE_ID=""
return 0
}
##########################
# Private helper methods #
##########################
_hostup_init() {
HOSTUP_API_KEY="${HOSTUP_API_KEY:-$(_readaccountconf_mutable HOSTUP_API_KEY)}"
HOSTUP_API_BASE="${HOSTUP_API_BASE:-$(_readaccountconf_mutable HOSTUP_API_BASE)}"
HOSTUP_TTL="${HOSTUP_TTL:-$(_readaccountconf_mutable HOSTUP_TTL)}"
HOSTUP_ZONE_ID="${HOSTUP_ZONE_ID:-$(_readaccountconf_mutable HOSTUP_ZONE_ID)}"
if [ -z "$HOSTUP_API_BASE" ]; then
HOSTUP_API_BASE="$HOSTUP_API_BASE_DEFAULT"
fi
if [ -z "$HOSTUP_API_KEY" ]; then
HOSTUP_API_KEY=""
_err "HOSTUP_API_KEY is not set."
_err "Please export your HostUp API key with read:dns and write:dns scopes."
return 1
fi
_saveaccountconf_mutable HOSTUP_API_KEY "$HOSTUP_API_KEY"
_saveaccountconf_mutable HOSTUP_API_BASE "$HOSTUP_API_BASE"
if [ -n "$HOSTUP_TTL" ]; then
_saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL"
fi
if [ -n "$HOSTUP_ZONE_ID" ]; then
_saveaccountconf_mutable HOSTUP_ZONE_ID "$HOSTUP_ZONE_ID"
fi
return 0
}
_hostup_detect_zone() {
fulldomain="$1"
if [ -n "$HOSTUP_ZONE_ID" ] && [ -n "$HOSTUP_ZONE_DOMAIN" ]; then
return 0
fi
HOSTUP_ZONE_DOMAIN=""
_debug "hostup_full_domain" "$fulldomain"
if [ -n "$HOSTUP_ZONE_ID" ] && [ -z "$HOSTUP_ZONE_DOMAIN" ]; then
# Attempt to fetch domain name for provided zone ID
if _hostup_fetch_zone_details "$HOSTUP_ZONE_ID"; then
return 0
fi
HOSTUP_ZONE_ID=""
fi
if ! _hostup_load_zones; then
return 1
fi
_domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)"
_debug "hostup_initial_candidate" "$_domain_candidate"
while [ -n "$_domain_candidate" ]; do
_debug "hostup_zone_candidate" "$_domain_candidate"
if _hostup_lookup_zone "$_domain_candidate"; then
HOSTUP_ZONE_DOMAIN="$_lookup_zone_domain"
HOSTUP_ZONE_ID="$_lookup_zone_id"
return 0
fi
case "$_domain_candidate" in
*.*) ;;
*) break ;;
esac
_domain_candidate="${_domain_candidate#*.}"
done
HOSTUP_ZONE_ID=""
return 1
}
_hostup_record_name() {
fulldomain="$1"
zonedomain="$2"
# Remove trailing dot, if any
fulldomain="${fulldomain%.}"
zonedomain="${zonedomain%.}"
if [ "$fulldomain" = "$zonedomain" ]; then
printf "%s" "@"
return 0
fi
suffix=".$zonedomain"
case "$fulldomain" in
*"$suffix")
printf "%s" "${fulldomain%"$suffix"}"
;;
*)
# Domain not within zone, fall back to full host
printf "%s" "$fulldomain"
;;
esac
}
_hostup_sanitize_name() {
name="$1"
if [ -z "$name" ] || [ "$name" = "." ]; then
printf "%s" "@"
return 0
fi
# Remove any trailing dot
name="${name%.}"
printf "%s" "$name"
}
_hostup_fqdn() {
domain="$1"
printf "%s" "${domain%.}"
}
_hostup_fetch_zone_details() {
zone_id="$1"
if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then
return 1
fi
zonedomain="$(printf "%s" "$_hostup_response" | _egrep_o '"domain":"[^"]*"' | sed -n '1p' | cut -d ':' -f 2 | tr -d '"')"
if [ -n "$zonedomain" ]; then
HOSTUP_ZONE_DOMAIN="$zonedomain"
return 0
fi
return 1
}
_hostup_load_zones() {
if ! _hostup_rest "GET" "/dns/zones" ""; then
return 1
fi
HOSTUP_ZONES_CACHE=""
data="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
while IFS= read -r line; do
case "$line" in
*'"domain_id"'*'"domain"'*)
zone_id="$(printf "%s" "$line" | _hostup_json_extract "domain_id")"
zone_domain="$(printf "%s" "$line" | _hostup_json_extract "domain")"
if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then
HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id}
"
_debug "hostup_zone_loaded" "$zone_domain|$zone_id"
fi
;;
esac
done <<EOF
$data
EOF
if [ -z "$HOSTUP_ZONES_CACHE" ]; then
_err "HostUp DNS API: no zones returned for the current API key."
return 1
fi
return 0
}
_hostup_lookup_zone() {
lookup_domain="$1"
_lookup_zone_id=""
_lookup_zone_domain=""
while IFS='|' read -r domain zone_id; do
[ -z "$domain" ] && continue
if [ "$domain" = "$lookup_domain" ]; then
_lookup_zone_domain="$domain"
_lookup_zone_id="$zone_id"
HOSTUP_ZONE_DOMAIN="$domain"
HOSTUP_ZONE_ID="$zone_id"
return 0
fi
done <<EOF
$HOSTUP_ZONES_CACHE
EOF
return 1
}
_hostup_find_record() {
zone_id="$1"
fqdn="$2"
txtvalue="$3"
if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then
return 1
fi
HOSTUP_RECORD_ID=""
records="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
while IFS= read -r line; do
# Normalize line to make TXT value matching reliable
line_clean="$(printf "%s" "$line" | tr -d '\r\n')"
line_value_clean="$(printf "%s" "$line_clean" | sed 's/\\"//g')"
case "$line_clean" in
*'"type":"TXT"'*'"name"'*'"value"'*)
name_value="$(_hostup_json_extract "name" "$line_clean")"
record_value="$(_hostup_json_extract "value" "$line_value_clean")"
_debug "hostup_record_raw" "$record_value"
if [ "${record_value#\"}" != "$record_value" ] && [ "${record_value%\"}" != "$record_value" ]; then
record_value="${record_value#\"}"
record_value="${record_value%\"}"
fi
if [ "${record_value#\'}" != "$record_value" ] && [ "${record_value%\'}" != "$record_value" ]; then
record_value="${record_value#\'}"
record_value="${record_value%\'}"
fi
record_value="$(printf "%s" "$record_value" | tr -d '\r\n')"
_debug "hostup_record_value" "$record_value"
if [ "$name_value" = "$fqdn" ] && [ "$record_value" = "$txtvalue" ]; then
record_id="$(_hostup_json_extract "id" "$line_clean")"
if [ -n "$record_id" ]; then
HOSTUP_RECORD_ID="$record_id"
return 0
fi
fi
;;
esac
done <<EOF
$records
EOF
return 1
}
_hostup_json_extract() {
key="$1"
input="${2:-$line}"
# First try to extract quoted values (strings)
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | _head_n 1)"
if [ -n "$quoted_match" ]; then
printf "%s" "$quoted_match" |
cut -d : -f2- |
sed 's/^"//' |
sed 's/"$//' |
sed 's/\\"/"/g'
return 0
fi
# Fallback for unquoted values (e.g., numeric IDs)
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | _head_n 1)"
if [ -n "$unquoted_match" ]; then
printf "%s" "$unquoted_match" |
cut -d : -f2- |
tr -d '", ' |
tr -d '\r\n'
return 0
fi
return 1
}
_hostup_json_escape() {
printf "%s" "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
_hostup_record_key() {
zone_id="$1"
domain="$2"
safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')"
safe_domain="$(printf "%s" "$domain" | _lower_case | sed 's/[^a-z0-9]/_/g')"
printf "%s_%s" "$safe_zone" "$safe_domain"
}
_hostup_save_record_id() {
zone_id="$1"
domain="$2"
record_id="$3"
key="$(_hostup_record_key "$zone_id" "$domain")"
_saveaccountconf_mutable "HOSTUP_RECORD_$key" "$record_id"
}
_hostup_get_saved_record_id() {
zone_id="$1"
domain="$2"
key="$(_hostup_record_key "$zone_id" "$domain")"
_readaccountconf_mutable "HOSTUP_RECORD_$key"
}
_hostup_clear_record_id() {
zone_id="$1"
domain="$2"
key="$(_hostup_record_key "$zone_id" "$domain")"
_clearaccountconf_mutable "HOSTUP_RECORD_$key"
}
_hostup_extract_record_id() {
record_id="$(_hostup_json_extract "id" "$1")"
if [ -n "$record_id" ]; then
printf "%s" "$record_id"
return 0
fi
printf "%s" "$1" | _egrep_o '"id":[0-9]+' | _head_n 1 | cut -d: -f2
}
_hostup_delete_record_by_id() {
zone_id="$1"
record_id="$2"
if ! _hostup_rest "DELETE" "/dns/zones/$zone_id/records/$record_id" ""; then
return 1
fi
if ! _contains "$_hostup_response" '"success":true'; then
return 1
fi
return 0
}
_hostup_rest() {
method="$1"
route="$2"
data="$3"
_hostup_response=""
export _H1="Authorization: Bearer $HOSTUP_API_KEY"
export _H2="Content-Type: application/json"
export _H3="Accept: application/json"
if [ "$method" = "GET" ]; then
_hostup_response="$(_get "$HOSTUP_API_BASE$route")"
else
_hostup_response="$(_post "$data" "$HOSTUP_API_BASE$route" "" "$method" "application/json")"
fi
ret="$?"
unset _H1
unset _H2
unset _H3
if [ "$ret" != "0" ]; then
_err "HTTP request failed for $route"
return 1
fi
http_status="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 "HTTP status" "$http_status"
_debug2 "_hostup_response" "$_hostup_response"
case "$http_status" in
200 | 201 | 204) return 0 ;;
401)
_err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions."
return 1
;;
403)
_err "HostUp API returned 403 Forbidden. The API key lacks required DNS scopes."
return 1
;;
404)
_err "HostUp API returned 404 Not Found for $route"
return 1
;;
429)
_err "HostUp API rate limit exceeded. Please retry later."
return 1
;;
*)
_err "HostUp API request failed with status $http_status"
return 1
;;
esac
}

244
dnsapi/dns_infoblox_uddi.sh Normal file
View File

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

View File

@@ -6,14 +6,16 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_infomaniak
Options:
INFOMANIAK_API_TOKEN API Token
Issues: github.com/acmesh-official/acme.sh/issues/3188
'
# To use this API you need visit the API dashboard of your account
# once logged into https://manager.infomaniak.com add /api/dashboard to the URL
#
# To use this API you need visit the API dashboard of your account.
# Note: the URL looks like this:
# https://manager.infomaniak.com/v3/<account_id>/api/dashboard
# Then generate a token with the scope Domain
# https://manager.infomaniak.com/v3/<account_id>/ng/profile/user/token/list
# Then generate a token with following scopes :
# - domain:read
# - dns:read
# - dns:write
# this is given as an environment variable INFOMANIAK_API_TOKEN
# base variables
@@ -65,33 +67,32 @@ dns_infomaniak_add() {
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
fqdn=${fulldomain#_acme-challenge.}
# guess which base domain to add record to
zone_and_id=$(_find_zone "$fqdn")
if [ -z "$zone_and_id" ]; then
_err "cannot find zone to modify"
zone=$(_get_zone "$fulldomain")
if [ -z "$zone" ]; then
_err "cannot find zone:<${zone}> to modify"
return 1
fi
zone=${zone_and_id% *}
domain_id=${zone_and_id#* }
# extract first part of domain
key=${fulldomain%."$zone"}
_debug "zone:$zone id:$domain_id key:$key"
_debug "key:$key"
_debug "txtvalue: $txtvalue"
# payload
data="{\"type\": \"TXT\", \"source\": \"$key\", \"target\": \"$txtvalue\", \"ttl\": $INFOMANIAK_TTL}"
# API call
response=$(_post "$data" "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record")
if [ -n "$response" ] && echo "$response" | _contains '"result":"success"'; then
_info "Record added"
_debug "Response: $response"
return 0
response=$(_post "$data" "${INFOMANIAK_API_URL}/2/zones/${zone}/records")
if [ -n "$response" ]; then
if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then
_info "Record added"
_debug "response: $response"
return 0
fi
fi
_err "could not create record"
_err "Could not create record."
_debug "Response: $response"
return 1
}
@@ -106,7 +107,7 @@ dns_infomaniak_rm() {
if [ -z "$INFOMANIAK_API_TOKEN" ]; then
INFOMANIAK_API_TOKEN=""
_err "Please provide a valid Infomaniak API token in variable INFOMANIAK_API_TOKEN"
_err "Please provide a valid Infomaniak API token in variable INFOMANIAK_API_TOKEN."
return 1
fi
@@ -138,63 +139,53 @@ dns_infomaniak_rm() {
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
fqdn=${fulldomain#_acme-challenge.}
# guess which base domain to add record to
zone_and_id=$(_find_zone "$fqdn")
if [ -z "$zone_and_id" ]; then
_err "cannot find zone to modify"
zone=$(_get_zone "$fulldomain")
if [ -z "$zone" ]; then
_err "cannot find zone:<$zone> to modify"
return 1
fi
zone=${zone_and_id% *}
domain_id=${zone_and_id#* }
# extract first part of domain
key=${fulldomain%."$zone"}
key=$(echo "$key" | _lower_case)
_debug "zone:$zone id:$domain_id key:$key"
_debug "zone:$zone"
_debug "key:$key"
# find previous record
# shellcheck disable=SC1004
record_id=$(_get "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record" | sed 's/.*"data":\[\(.*\)\]}/\1/; s/},{/}\
{/g' | sed -n 's/.*"id":"*\([0-9]*\)"*.*"source_idn":"'"$fulldomain"'".*"target_idn":"'"$txtvalue"'".*/\1/p')
if [ -z "$record_id" ]; then
_err "could not find record to delete"
return 1
fi
# shellcheck disable=SC2086
response=$(_get "${INFOMANIAK_API_URL}/2/zones/${zone}/records" | sed 's/.*"data":\[\(.*\)\]}/\1/; s/},{/}{/g')
record_id=$(echo "$response" | sed -n 's/.*"id":"*\([0-9]*\)"*.*"source":"'"$key"'".*"target":"\\"'"$txtvalue"'\\"".*/\1/p')
_debug "key: $key"
_debug "txtvalue: $txtvalue"
_debug "record_id: $record_id"
# API call
response=$(_post "" "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record/$record_id" "" DELETE)
if [ -n "$response" ] && echo "$response" | _contains '"result":"success"'; then
_info "Record deleted"
return 0
if [ -z "$record_id" ]; then
_err "could not find record to delete"
_debug "response: $response"
return 1
fi
_err "could not delete record"
# API call
response=$(_post "" "${INFOMANIAK_API_URL}/2/zones/${zone}/records/${record_id}" "" DELETE)
if [ -n "$response" ]; then
if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then
_info "Record deleted"
return 0
fi
fi
_err "Could not delete record."
_debug "Response: $response"
return 1
}
#################### Private functions below ##################################
_get_domain_id() {
_get_zone() {
domain="$1"
# Whatever the domain is, you can get the fqdn with the following.
# shellcheck disable=SC1004
_get "${INFOMANIAK_API_URL}/1/product?service_name=domain&customer_name=$domain" | sed 's/.*"data":\[{\(.*\)}\]}/\1/; s/,/\
/g' | sed -n 's/^"id":\(.*\)/\1/p'
}
_find_zone() {
zone="$1"
# find domain in list, removing . parts sequentialy
while _contains "$zone" '\.'; do
_debug "testing $zone"
id=$(_get_domain_id "$zone")
if [ -n "$id" ]; then
echo "$zone $id"
return
fi
zone=${zone#*.}
done
response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones" | sed 's/.*\[{"fqdn"\:"\(.*\)/\1/')
echo "${response%%\"*}"
}

View File

@@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_inwx
Options:
INWX_User Username
INWX_Password Password
INWX_Shared_Secret 2 Factor Authentication Shared Secret (optional requires oathtool)
'
# Dependencies:
@@ -110,11 +111,17 @@ dns_inwx_rm() {
<string>%s</string>
</value>
</member>
<member>
<name>content</name>
<value>
<string>%s</string>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>' "$_domain" "$_sub_domain")
</methodCall>' "$_domain" "$_sub_domain" "$txtvalue")
response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
if ! _contains "$response" "Command completed successfully"; then
@@ -125,7 +132,7 @@ dns_inwx_rm() {
if ! printf "%s" "$response" | grep "count" >/dev/null; then
_info "Do not need to delete record"
else
_record_id=$(printf '%s' "$response" | _egrep_o '.*(<member><name>record){1}(.*)([0-9]+){1}' | _egrep_o '<name>id<\/name><value><int>[0-9]+' | _egrep_o '[0-9]+')
_record_id=$(printf '%s' "$response" | _egrep_o '.*(<member><name>record){1}(.*)([0-9]+){1}' | _egrep_o '<name>id<\/name><value><string>[0-9]+' | _egrep_o '[0-9]+')
_info "Deleting record"
_inwx_delete_record "$_record_id"
fi
@@ -324,7 +331,7 @@ _inwx_delete_record() {
<member>
<name>id</name>
<value>
<int>%s</int>
<string>%s</string>
</value>
</member>
</struct>
@@ -362,7 +369,7 @@ _inwx_update_record() {
<member>
<name>id</name>
<value>
<int>%s</int>
<string>%s</string>
</value>
</member>
</struct>

View File

@@ -53,6 +53,8 @@ dns_me_add() {
_info "Added"
#todo: check if the record takes effect
return 0
elif printf -- "%s" "$response" | grep -q "already exists"; then
_info "Record already exists, skipping."
else
_err "Add txt record error."
return 1

109
dnsapi/dns_mgwm.sh Normal file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_mgwm_info='mgw-media.de
Site: mgw-media.de
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mgwm
Options:
MGWM_CUSTOMER Your customer number
MGWM_API_HASH Your API Hash
Issues: github.com/acmesh-official/acme.sh/issues/6669
'
# Base URL for the mgw-media.de API
MGWM_API_BASE="https://api.mgw-media.de/record"
######## Public functions #####################
# This function is called by acme.sh to add a TXT record.
dns_mgwm_add() {
fulldomain=$1
txtvalue=$2
_info "Using mgw-media.de DNS API for domain $fulldomain (add record)"
_debug "fulldomain: $fulldomain"
_debug "txtvalue: $txtvalue"
# Call the new private function to handle the API request.
# The 'add' action, fulldomain, type 'txt' and txtvalue are passed.
if _mgwm_request "add" "$fulldomain" "txt" "$txtvalue"; then
_info "TXT record for $fulldomain successfully added via mgw-media.de API."
_sleep 10 # Wait briefly for DNS propagation, a common practice in DNS-01 hooks.
return 0
else
# Error message already logged by _mgwm_request, but a specific one here helps.
_err "mgwm_add: Failed to add TXT record for $fulldomain."
return 1
fi
}
# This function is called by acme.sh to remove a TXT record after validation.
dns_mgwm_rm() {
fulldomain=$1
txtvalue=$2 # This txtvalue is now used to identify the specific record to be removed.
_info "Removing TXT record for $fulldomain using mgw-media.de DNS API (remove record)"
_debug "fulldomain: $fulldomain"
_debug "txtvalue: $txtvalue"
# Call the new private function to handle the API request.
# The 'rm' action, fulldomain, type 'txt' and txtvalue are passed.
if _mgwm_request "rm" "$fulldomain" "txt" "$txtvalue"; then
_info "TXT record for $fulldomain successfully removed via mgw-media.de API."
return 0
else
# Error message already logged by _mgwm_request, but a specific one here helps.
_err "mgwm_rm: Failed to remove TXT record for $fulldomain."
return 1
fi
}
#################### Private functions below ##################################
# _mgwm_request() encapsulates the API call logic, including
# loading credentials, setting the Authorization header, and executing the request.
# Arguments:
# $1: action (e.g., "add", "rm")
# $2: fulldomain
# $3: type (e.g., "txt")
# $4: content (the txtvalue)
_mgwm_request() {
_action="$1"
_fulldomain="$2"
_type="$3"
_content="$4"
_debug "Calling _mgwm_request for action: $_action, domain: $_fulldomain, type: $_type, content: $_content"
# Load credentials from environment or acme.sh config
MGWM_CUSTOMER="${MGWM_CUSTOMER:-$(_readaccountconf_mutable MGWM_CUSTOMER)}"
MGWM_API_HASH="${MGWM_API_HASH:-$(_readaccountconf_mutable MGWM_API_HASH)}"
# Check if credentials are set
if [ -z "$MGWM_CUSTOMER" ] || [ -z "$MGWM_API_HASH" ]; then
_err "You didn't specify one or more of MGWM_CUSTOMER or MGWM_API_HASH."
_err "Please check these environment variables and try again."
return 1
fi
# Save credentials for automatic renewal and future calls
_saveaccountconf_mutable MGWM_CUSTOMER "$MGWM_CUSTOMER"
_saveaccountconf_mutable MGWM_API_HASH "$MGWM_API_HASH"
# Create the Basic Auth Header. acme.sh's _base64 function is used for encoding.
_credentials="$(printf "%s:%s" "$MGWM_CUSTOMER" "$MGWM_API_HASH" | _base64)"
export _H1="Authorization: Basic $_credentials"
_debug "Set Authorization Header: Basic <credentials_encoded>" # Log debug message without sensitive credentials
# Construct the API URL based on the action and provided parameters.
_request_url="${MGWM_API_BASE}/${_action}/${_fulldomain}/${_type}/${_content}"
_debug "Constructed mgw-media.de API URL for action '$_action': ${_request_url}"
# Execute the HTTP GET request with the Authorization Header.
# The 5th parameter of _get is where acme.sh expects custom HTTP headers like Authorization.
response="$(_get "$_request_url")"
_debug "mgw-media.de API response for action '$_action': $response"
# Check the API response for success. The API returns "OK" on success.
if [ "$response" = "OK" ]; then
_info "mgw-media.de API action '$_action' for record '$_fulldomain' successful."
return 0
else
_err "Failed mgw-media.de API action '$_action' for record '$_fulldomain'. Unexpected API Response: '$response'"
return 1
fi
}

View File

@@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_nsupdate
Options:
NSUPDATE_SERVER Server hostname. Default: "localhost".
NSUPDATE_SERVER_PORT Server port. Default: "53".
NSUPDATE_KEY File path to TSIG key.
NSUPDATE_KEY File path to TSIG key. Default: "". Optional.
NSUPDATE_ZONE Domain zone to update. Optional.
'
@@ -22,8 +22,6 @@ dns_nsupdate_add() {
NSUPDATE_ZONE="${NSUPDATE_ZONE:-$(_readaccountconf_mutable NSUPDATE_ZONE)}"
NSUPDATE_OPT="${NSUPDATE_OPT:-$(_readaccountconf_mutable NSUPDATE_OPT)}"
_checkKeyFile || return 1
# save the dns server and key to the account conf file.
_saveaccountconf_mutable NSUPDATE_SERVER "${NSUPDATE_SERVER}"
_saveaccountconf_mutable NSUPDATE_SERVER_PORT "${NSUPDATE_SERVER_PORT}"
@@ -33,27 +31,52 @@ dns_nsupdate_add() {
[ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost"
[ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53
[ -n "${NSUPDATE_KEY}" ] || NSUPDATE_KEY=""
[ -n "${NSUPDATE_OPT}" ] || NSUPDATE_OPT=""
NSUPDATE_SERVER_LIST=$(printf "%s" "$NSUPDATE_SERVER" | tr ',' ' ')
_info "adding ${fulldomain}. 60 in txt \"${txtvalue}\""
[ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d"
[ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D"
if [ -z "${NSUPDATE_ZONE}" ]; then
#shellcheck disable=SC2086
nsupdate -k "${NSUPDATE_KEY}" $nsdebug $NSUPDATE_OPT <<EOF
server ${NSUPDATE_SERVER} ${NSUPDATE_SERVER_PORT}
for NS_SERVER in $NSUPDATE_SERVER_LIST; do
_info "Updating DNS server: $NS_SERVER"
if [ -z "${NSUPDATE_ZONE}" ]; then
#shellcheck disable=SC2086
if [ -z "${NSUPDATE_KEY}" ]; then
nsupdate $nsdebug $NSUPDATE_OPT <<EOF
server ${NS_SERVER} ${NSUPDATE_SERVER_PORT}
update add ${fulldomain}. 60 in txt "${txtvalue}"
send
EOF
else
#shellcheck disable=SC2086
nsupdate -k "${NSUPDATE_KEY}" $nsdebug $NSUPDATE_OPT <<EOF
server ${NSUPDATE_SERVER} ${NSUPDATE_SERVER_PORT}
else
nsupdate -k "${NSUPDATE_KEY}" $nsdebug $NSUPDATE_OPT <<EOF
server ${NS_SERVER} ${NSUPDATE_SERVER_PORT}
update add ${fulldomain}. 60 in txt "${txtvalue}"
send
EOF
fi
else
#shellcheck disable=SC2086
if [ -z "${NSUPDATE_KEY}" ]; then
nsupdate $nsdebug $NSUPDATE_OPT <<EOF
server ${NS_SERVER} ${NSUPDATE_SERVER_PORT}
zone ${NSUPDATE_ZONE}.
update add ${fulldomain}. 60 in txt "${txtvalue}"
send
EOF
fi
else
nsupdate -k "${NSUPDATE_KEY}" $nsdebug $NSUPDATE_OPT <<EOF
server ${NS_SERVER} ${NSUPDATE_SERVER_PORT}
zone ${NSUPDATE_ZONE}.
update add ${fulldomain}. 60 in txt "${txtvalue}"
send
EOF
fi
fi
done
if [ $? -ne 0 ]; then
_err "error updating domain"
return 1
@@ -72,28 +95,53 @@ dns_nsupdate_rm() {
NSUPDATE_ZONE="${NSUPDATE_ZONE:-$(_readaccountconf_mutable NSUPDATE_ZONE)}"
NSUPDATE_OPT="${NSUPDATE_OPT:-$(_readaccountconf_mutable NSUPDATE_OPT)}"
_checkKeyFile || return 1
[ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost"
[ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53
[ -n "${NSUPDATE_KEY}" ] || NSUPDATE_KEY=""
NSUPDATE_SERVER_LIST=$(printf "%s" "$NSUPDATE_SERVER" | tr ',' ' ')
_info "removing ${fulldomain}. txt"
[ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d"
[ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D"
if [ -z "${NSUPDATE_ZONE}" ]; then
#shellcheck disable=SC2086
nsupdate -k "${NSUPDATE_KEY}" $nsdebug $NSUPDATE_OPT <<EOF
server ${NSUPDATE_SERVER} ${NSUPDATE_SERVER_PORT}
for NS_SERVER in $NSUPDATE_SERVER_LIST; do
_info "Updating DNS server: $NS_SERVER"
if [ -z "${NSUPDATE_ZONE}" ]; then
#shellcheck disable=SC2086
if [ -z "${NSUPDATE_KEY}" ]; then
nsupdate $nsdebug $NSUPDATE_OPT <<EOF
server ${NS_SERVER} ${NSUPDATE_SERVER_PORT}
update delete ${fulldomain}. txt
send
EOF
else
#shellcheck disable=SC2086
nsupdate -k "${NSUPDATE_KEY}" $nsdebug $NSUPDATE_OPT <<EOF
server ${NSUPDATE_SERVER} ${NSUPDATE_SERVER_PORT}
else
nsupdate -k "${NSUPDATE_KEY}" $nsdebug $NSUPDATE_OPT <<EOF
server ${NS_SERVER} ${NSUPDATE_SERVER_PORT}
update delete ${fulldomain}. txt
send
EOF
fi
else
#shellcheck disable=SC2086
if [ -z "${NSUPDATE_KEY}" ]; then
nsupdate $nsdebug $NSUPDATE_OPT <<EOF
server ${NS_SERVER} ${NSUPDATE_SERVER_PORT}
zone ${NSUPDATE_ZONE}.
update delete ${fulldomain}. txt
send
EOF
fi
else
nsupdate -k "${NSUPDATE_KEY}" $nsdebug $NSUPDATE_OPT <<EOF
server ${NS_SERVER} ${NSUPDATE_SERVER_PORT}
zone ${NSUPDATE_ZONE}.
update delete ${fulldomain}. txt
send
EOF
fi
fi
done
if [ $? -ne 0 ]; then
_err "error updating domain"
return 1
@@ -101,16 +149,3 @@ EOF
return 0
}
#################### Private functions below ##################################
_checkKeyFile() {
if [ -z "${NSUPDATE_KEY}" ]; then
_err "you must specify a path to the nsupdate key file"
return 1
fi
if [ ! -r "${NSUPDATE_KEY}" ]; then
_err "key ${NSUPDATE_KEY} is unreadable"
return 1
fi
}

View File

@@ -4,8 +4,8 @@ dns_omglol_info='omg.lol
Site: omg.lol
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_omglol
Options:
OMG_ApiKey API Key. This is accessible from the bottom of the account page at https://home.omg.lol/account
OMG_Address Address. This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard
OMG_ApiKey - API Key. This is accessible from the bottom of the account page at https://home.omg.lol/account
OMG_Address - Address. This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard
Issues: github.com/acmesh-official/acme.sh/issues/5299
Author: @Kholin <kholin+acme.omglolapi@omg.lol>
'
@@ -35,7 +35,7 @@ dns_omglol_add() {
_debug "omg.lol Address" "$OMG_Address"
omg_validate "$OMG_ApiKey" "$OMG_Address" "$fulldomain"
if [ ! $? ]; then
if [ 1 = $? ]; then
return 1
fi
@@ -67,7 +67,7 @@ dns_omglol_rm() {
_debug "omg.lol Address" "$OMG_Address"
omg_validate "$OMG_ApiKey" "$OMG_Address" "$fulldomain"
if [ ! $? ]; then
if [ 1 = $? ]; then
return 1
fi
@@ -100,18 +100,49 @@ omg_validate() {
fi
_endswith "$fulldomain" "omg.lol"
if [ ! $? ]; then
if [ 1 = $? ]; then
_err "Domain name requested is not under omg.lol"
return 1
fi
_endswith "$fulldomain" "$omg_address.omg.lol"
if [ ! $? ]; then
if [ 1 = $? ]; then
_err "Domain name is not a subdomain of provided omg.lol address $omg_address"
return 1
fi
_debug "Required environment parameters are all present"
omg_testconnect "$omg_apikey" "$omg_address"
if [ 1 = $? ]; then
_err "Authentication to omg.lol for address $omg_address using provided API key failed"
return 1
fi
_debug "Required environment parameters are all present and validated"
}
# Validate that the address and API key are both correct and associated to each other
omg_testconnect() {
omg_apikey=$1
omg_address=$2
_debug2 "Function" "omg_testconnect"
_secure_debug2 "omg.lol API key" "$omg_apikey"
_debug2 "omg.lol Address" "$omg_address"
authheader="$(_createAuthHeader "$omg_apikey")"
export _H1="$authheader"
endpoint="https://api.omg.lol/address/$omg_address/info"
_debug2 "Endpoint for validation" "$endpoint"
response=$(_get "$endpoint" "" 30)
_jsonResponseCheck "$response" "status_code" 200
if [ 1 = $? ]; then
_debug2 "Failed to query omg.lol for $omg_address with provided API key"
_secure_debug2 "API Key" "omg_apikey"
_secure_debug3 "Raw response" "$response"
return 1
fi
}
# Add (or modify) an entry for a new ACME query

158
dnsapi/dns_opusdns.sh Executable file
View File

@@ -0,0 +1,158 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_opusdns_info='OpusDNS.com
Site: OpusDNS.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_opusdns
Options:
OPUSDNS_API_Key API Key. Can be created at https://dashboard.opusdns.com/settings/api-keys
OPUSDNS_API_Endpoint API Endpoint URL. Default "https://api.opusdns.com". Optional.
OPUSDNS_TTL TTL for DNS challenge records in seconds. Default "60". Optional.
Issues: github.com/acmesh-official/acme.sh/issues/XXXX
Author: OpusDNS Team <https://github.com/opusdns>
'
OPUSDNS_API_Endpoint_Default="https://api.opusdns.com"
OPUSDNS_TTL_Default=60
######## Public functions ###########
# Add DNS TXT record
dns_opusdns_add() {
fulldomain=$1
txtvalue=$2
_info "Using OpusDNS DNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _opusdns_init; then
return 1
fi
if ! _get_zone "$fulldomain"; then
return 1
fi
_info "Zone: $_zone, Record: $_record_name"
if ! _opusdns_api PATCH "/v1/dns/$_zone/records" "{\"ops\":[{\"op\":\"upsert\",\"record\":{\"name\":\"$_record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$txtvalue\\\"\"}}]}"; then
_err "Failed to add TXT record"
return 1
fi
_info "TXT record added successfully"
return 0
}
# Remove DNS TXT record
dns_opusdns_rm() {
fulldomain=$1
txtvalue=$2
_info "Removing OpusDNS DNS record"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _opusdns_init; then
return 1
fi
if ! _get_zone "$fulldomain"; then
_err "Zone not found, cleanup skipped"
return 0
fi
_info "Zone: $_zone, Record: $_record_name"
if ! _opusdns_api PATCH "/v1/dns/$_zone/records" "{\"ops\":[{\"op\":\"remove\",\"record\":{\"name\":\"$_record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$txtvalue\\\"\"}}]}"; then
_err "Warning: Failed to remove TXT record"
return 0
fi
_info "TXT record removed successfully"
return 0
}
######## Private functions ###########
# Initialize and validate configuration
_opusdns_init() {
OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}"
OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}"
OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}"
if [ -z "$OPUSDNS_API_Key" ]; then
_err "OPUSDNS_API_Key not set"
return 1
fi
[ -z "$OPUSDNS_API_Endpoint" ] && OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default"
[ -z "$OPUSDNS_TTL" ] && OPUSDNS_TTL="$OPUSDNS_TTL_Default"
_saveaccountconf_mutable OPUSDNS_API_Key "$OPUSDNS_API_Key"
_saveaccountconf_mutable OPUSDNS_API_Endpoint "$OPUSDNS_API_Endpoint"
_saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL"
_debug "Endpoint: $OPUSDNS_API_Endpoint"
return 0
}
# Make API request
# Usage: _opusdns_api METHOD PATH [DATA]
_opusdns_api() {
method=$1
path=$2
data=$3
export _H1="X-Api-Key: $OPUSDNS_API_Key"
export _H2="Content-Type: application/json"
url="$OPUSDNS_API_Endpoint$path"
_debug2 "API: $method $url"
[ -n "$data" ] && _debug2 "Data: $data"
if [ -n "$data" ]; then
response=$(_post "$data" "$url" "" "$method")
else
response=$(_get "$url")
fi
if [ $? -ne 0 ]; then
_err "API request failed"
_debug "Response: $response"
return 1
fi
_debug2 "Response: $response"
return 0
}
# Detect zone from FQDN
# Sets: _zone, _record_name
_get_zone() {
domain=$(echo "$1" | sed 's/\.$//')
_debug "Finding zone for: $domain"
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
_err "No valid zone found for: $domain"
return 1
fi
_debug "Trying: $h"
if _opusdns_api GET "/v1/dns/$h" && _contains "$response" '"dnssec_status"'; then
_zone="$h"
_record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
[ -z "$_record_name" ] && _record_name="@"
return 0
fi
p="$i"
i=$(_math "$i" + 1)
done
}

216
dnsapi/dns_qc.sh Executable file
View File

@@ -0,0 +1,216 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_qc_info='QUIC.cloud
Site: quic.cloud
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_qc
Options:
QC_API_KEY QC API Key
QC_API_EMAIL Your account email
'
QC_Api="https://api.quic.cloud/v2"
######## Public functions #####################
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_qc_add() {
fulldomain=$1
txtvalue=$2
_debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue"
QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}"
QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}"
if [ "$QC_API_KEY" ]; then
_saveaccountconf_mutable QC_API_KEY "$QC_API_KEY"
else
_err "You didn't specify a QUIC.cloud api key as QC_API_KEY."
_err "You can get yours from here https://my.quic.cloud/up/api."
return 1
fi
if ! _contains "$QC_API_EMAIL" "@"; then
_err "It seems that the QC_API_EMAIL=$QC_API_EMAIL is not a valid email address."
_err "Please check and retry."
return 1
fi
#save the api key and email to the account conf file.
_saveaccountconf_mutable QC_API_EMAIL "$QC_API_EMAIL"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain during add"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug "Getting txt records"
_qc_rest GET "zones/${_domain_id}/records"
if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then
_err "Error failed response from QC GET: $response"
return 1
fi
# For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so
# we can not use updating anymore.
# count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
# _debug count "$count"
# if [ "$count" = "0" ]; then
_info "Adding txt record"
if _qc_rest POST "zones/$_domain_id/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":1800}"; then
if _contains "$response" "$txtvalue"; then
_info "Added txt record, OK"
return 0
elif _contains "$response" "Same record already exists"; then
_info "txt record already exists, OK"
return 0
else
_err "Add txt record error: $response"
return 1
fi
fi
_err "Add txt record error: POST failed: $response"
return 1
}
#fulldomain txtvalue
dns_qc_rm() {
fulldomain=$1
txtvalue=$2
_debug "Enter dns_qc_rm fulldomain: $fulldomain, txtvalue: $txtvalue"
QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}"
QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain during rm"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug "Getting txt records"
_qc_rest GET "zones/${_domain_id}/records"
if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then
_err "Error rm GET response: $response"
return 1
fi
_debug "Pre-jq response:" "$response"
# Do not use jq or subsequent code
#response=$(echo "$response" | jq ".result[] | select(.id) | select(.content == \"$txtvalue\") | select(.type == \"TXT\")")
#_debug "get txt response" "$response"
#if [ "${response}" = "" ]; then
# _info "Don't need to remove txt records."
# return 0
#fi
#record_id=$(echo "$response" | grep \"id\" | awk -F ' ' '{print $2}' | sed 's/,$//')
#_debug "txt record_id" "$record_id"
#Instead of jq
array=$(echo "$response" | grep -o '\[[^]]*\]' | sed 's/^\[\(.*\)\]$/\1/')
if [ -z "$array" ]; then
_err "Expected array in QC response: $response"
return 1
fi
# Temporary file to hold matched content (one per line)
tmpfile=$(_mktemp)
echo "$array" | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' >"$tmpfile"
record_id=""
while IFS= read -r obj || [ -n "$obj" ]; do
if echo "$obj" | grep -q '"TXT"' && echo "$obj" | grep -q '"id"' && echo "$obj" | grep -q "$txtvalue"; then
_debug "response includes" "$obj"
record_id=$(echo "$obj" | sed 's/^\"id\":\([0-9]\+\).*/\1/')
break
fi
done <"$tmpfile"
rm "$tmpfile"
if [ -z "$record_id" ]; then
_info "TXT record, or $txtvalue not found, nothing to remove"
return 0
fi
#End of jq replacement
if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then
_info "Delete txt record error."
return 1
fi
_info "TXT Record ID: $record_id successfully deleted"
return 0
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_id=sdjkglgdfewsdfg
_get_root() {
domain=$1
i=1
p=1
h=$(printf "%s" "$domain" | cut -d . -f2-)
_debug h "$h"
if [ -z "$h" ]; then
_err "$h ($domain) is an invalid domain"
return 1
fi
if ! _qc_rest GET "zones"; then
_err "qc_rest failed"
return 1
fi
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" "\"name\":\"$h.\""; then
_domain_id=$h
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
fi
_err "Empty domain_id $h"
return 1
fi
_err "Missing domain_id $h"
return 1
}
_qc_rest() {
m=$1
ep="$2"
data="$3"
_debug "$ep"
email_trimmed=$(echo "$QC_API_EMAIL" | tr -d '"')
token_trimmed=$(echo "$QC_API_KEY" | tr -d '"')
export _H1="Content-Type: application/json"
export _H2="X-Auth-Email: $email_trimmed"
export _H3="X-Auth-Key: $token_trimmed"
if [ "$m" != "GET" ]; then
_debug data "$data"
response="$(_post "$data" "$QC_Api/$ep" "" "$m")"
else
response="$(_get "$QC_Api/$ep")"
fi
if [ "$?" != "0" ]; then
_err "error $ep"
return 1
fi
_debug2 response "$response"
return 0
}

220
dnsapi/dns_sitehost.sh Executable file
View File

@@ -0,0 +1,220 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_sitehost_info='SiteHost
Site: sitehost.nz
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sitehost
Options:
SITEHOST_API_KEY API Key
SITEHOST_CLIENT_ID Client ID. The numeric client ID for your SiteHost account.
Issues: github.com/acmesh-official/acme.sh/issues/6892
Author: Jordan Russell <jordanbrussell@gmail.com>
'
SITEHOST_API="https://api.sitehost.nz/1.5"
######## Public functions #####################
# Usage: dns_sitehost_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_sitehost_add() {
fulldomain=$1
txtvalue=$2
if ! _sitehost_load_creds; then
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"
# SiteHost expects the full record name as the name parameter
_info "Adding TXT record for ${fulldomain}"
if _sitehost_rest POST "dns/add_record.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)&type=TXT&name=$(printf '%s' "${fulldomain}" | _url_encode)&content=$(printf '%s' "${txtvalue}" | _url_encode)"; then
if _contains "$response" '"status":true'; then
_info "TXT record added successfully."
return 0
fi
fi
_err "Could not add TXT record for ${fulldomain}"
_err "$response"
return 1
}
# Usage: dns_sitehost_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Remove the txt record after validation.
dns_sitehost_rm() {
fulldomain=$1
txtvalue=$2
if ! _sitehost_load_creds; then
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"
_debug "Getting TXT records for ${_domain}"
if ! _sitehost_rest GET "dns/list_records.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)"; then
_err "Could not list DNS records"
_err "$response"
return 1
fi
if ! _contains "$response" '"status":true'; then
_err "Error listing DNS records"
_err "$response"
return 1
fi
# Extract record ID matching our fulldomain, type TXT, and txtvalue
# Response format: {"return":[{"id":"123","name":"...","type":"TXT","content":"..."},...]}
# SiteHost returns flat single-line JSON objects in the records array
# Escape regex metacharacters in values before grep matching
_fulldomain_grep="$(printf "%s" "$fulldomain" | sed 's/[][\\.^$*]/\\&/g')"
_txtvalue_grep="$(printf "%s" "$txtvalue" | sed 's/[][\\.^$*]/\\&/g')"
# Use field-specific matching to avoid false positives from substring matches
_record_id="$(echo "$response" | _egrep_o '\{[^}]*\}' | grep '"name" *: *"'"${_fulldomain_grep}"'"' | grep '"type" *: *"TXT"' | grep '"content" *: *"'"${_txtvalue_grep}"'"' | _head_n 1 | _egrep_o '"id" *: *"?[0-9]+"?' | _egrep_o '[0-9]+')"
if [ -z "$_record_id" ]; then
_info "TXT record not found, nothing to remove."
return 0
fi
_debug _record_id "$_record_id"
_info "Deleting TXT record ${_record_id} for ${fulldomain}"
if _sitehost_rest POST "dns/delete_record.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)&record_id=$(printf '%s' "${_record_id}" | _url_encode)"; then
if _contains "$response" '"status":true'; then
_info "TXT record deleted successfully."
return 0
fi
fi
_err "Could not delete TXT record for ${fulldomain}"
_err "$response"
return 1
}
#################### Private functions below ##################################
_sitehost_load_creds() {
SITEHOST_API_KEY="${SITEHOST_API_KEY:-$(_readaccountconf_mutable SITEHOST_API_KEY)}"
SITEHOST_CLIENT_ID="${SITEHOST_CLIENT_ID:-$(_readaccountconf_mutable SITEHOST_CLIENT_ID)}"
if [ -z "$SITEHOST_API_KEY" ] || [ -z "$SITEHOST_CLIENT_ID" ]; then
SITEHOST_API_KEY=""
SITEHOST_CLIENT_ID=""
_err "You didn't specify SITEHOST_API_KEY and/or SITEHOST_CLIENT_ID."
_err "Please export them and try again."
return 1
fi
_saveaccountconf_mutable SITEHOST_API_KEY "$SITEHOST_API_KEY"
_saveaccountconf_mutable SITEHOST_CLIENT_ID "$SITEHOST_CLIENT_ID"
return 0
}
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
_get_root() {
domain=$1
_debug "Getting domain list"
# Fetch ALL pages of domains first so we can match the most specific zone
# (a more specific zone on a later page must take precedence over a broader match)
_all_domains=""
_page=1
while true; do
if ! _sitehost_rest GET "dns/list_domains.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&filters%5Bpage_number%5D=${_page}"; then
_err "Could not list domains"
return 1
fi
if ! _contains "$response" '"status":true'; then
_err "Error listing domains"
_err "$response"
return 1
fi
_all_domains="${_all_domains} ${response}"
_total_pages=$(echo "$response" | _egrep_o '"total_pages" *: *[0-9]+' | _egrep_o '[0-9]+')
if [ -z "$_total_pages" ] || [ "$_page" -ge "$_total_pages" ]; then
break
fi
_page=$(_math "$_page" + 1)
done
# Try each subdomain level, most specific first
_i=1
_p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "${_i}"-100)
_debug h "$h"
if [ -z "$h" ]; then
return 1
fi
if echo "$_all_domains" | grep -F "\"${h}\"" >/dev/null 2>&1; then
if [ "$_i" = "1" ]; then
# DNS alias mode - fulldomain is the zone itself
_sub_domain=""
else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"${_p}")
fi
_domain="${h}"
return 0
fi
_p="${_i}"
_i=$(_math "$_i" + 1)
done
return 1
}
# Usage: _sitehost_rest method endpoint data
_sitehost_rest() {
m="$1"
ep="$2"
data="$3"
url="${SITEHOST_API}/${ep}"
_debug url "$url"
_apikey="$(printf "%s" "${SITEHOST_API_KEY}" | _url_encode)"
if [ "$m" = "GET" ]; then
response="$(_get "${url}?apikey=${_apikey}&${data}")"
else
_debug2 data "$data"
response="$(_post "apikey=${_apikey}&${data}" "$url")"
fi
if [ "$?" != "0" ]; then
_err "error ${ep}"
return 1
fi
response="$(printf '%s' "$response" | tr -d '\r')"
_debug2 response "$response"
return 0
}

309
dnsapi/dns_sotoon.sh Normal file
View File

@@ -0,0 +1,309 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_sotoon_info='Sotoon.ir
Site: Sotoon.ir
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sotoon
Options:
Sotoon_Token API Token
Sotoon_WorkspaceUUID Workspace UUID
Issues: github.com/acmesh-official/acme.sh/issues/6656
Author: Erfan Gholizade
'
SOTOON_API_URL="https://api.sotoon.ir/delivery/v2.1/global"
######## Public functions #####################
#Adding the txt record for validation.
#Usage: dns_sotoon_add fulldomain TXT_record
#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_sotoon_add() {
fulldomain=$1
txtvalue=$2
_info_sotoon "Using Sotoon"
Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}"
Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}"
if [ -z "$Sotoon_Token" ]; then
_err_sotoon "You didn't specify \"Sotoon_Token\" token yet."
_err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/tokens"
return 1
fi
if [ -z "$Sotoon_WorkspaceUUID" ]; then
_err_sotoon "You didn't specify \"Sotoon_WorkspaceUUID\" Workspace UUID yet."
_err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces"
return 1
fi
#save the info to the account conf file.
_saveaccountconf_mutable Sotoon_Token "$Sotoon_Token"
_saveaccountconf_mutable Sotoon_WorkspaceUUID "$Sotoon_WorkspaceUUID"
_debug_sotoon "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err_sotoon "invalid domain"
return 1
fi
_info_sotoon "Adding record"
_debug_sotoon _domain_id "$_domain_id"
_debug_sotoon _sub_domain "$_sub_domain"
_debug_sotoon _domain "$_domain"
# First, GET the current domain zone to check for existing TXT records
# This is needed for wildcard certs which require multiple TXT values
_info_sotoon "Checking for existing TXT records"
if ! _sotoon_rest GET "$_domain_id"; then
_err_sotoon "Failed to get domain zone"
return 1
fi
# Check if there are existing TXT records for this subdomain
_existing_txt=""
if _contains "$response" "\"$_sub_domain\""; then
_debug_sotoon "Found existing records for $_sub_domain"
# Extract existing TXT values from the response
# The format is: "_acme-challenge":[{"TXT":"value1","type":"TXT","ttl":10},{"TXT":"value2",...}]
_existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://")
_debug_sotoon "Existing TXT records: $_existing_txt"
fi
# Build the new record entry
_new_record="{\"TXT\":\"$txtvalue\",\"type\":\"TXT\",\"ttl\":120}"
# If there are existing records, append to them; otherwise create new array
if [ -n "$_existing_txt" ] && [ "$_existing_txt" != "[]" ] && [ "$_existing_txt" != "null" ]; then
# Check if this exact TXT value already exists (avoid duplicates)
if _contains "$_existing_txt" "\"$txtvalue\""; then
_info_sotoon "TXT record already exists, skipping"
return 0
fi
# Remove the closing bracket and append new record
_combined_records="$(echo "$_existing_txt" | sed 's/]$//'),$_new_record]"
_debug_sotoon "Combined records: $_combined_records"
else
# No existing records, create new array
_combined_records="[$_new_record]"
fi
# Prepare the DNS record data in Kubernetes CRD format
_dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_combined_records}}}"
_debug_sotoon "DNS record payload: $_dns_record"
# Use PATCH to update/add the record to the domain zone
_info_sotoon "Updating domain zone $_domain_id with TXT record"
if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then
if _contains "$response" "$txtvalue" || _contains "$response" "\"$_sub_domain\""; then
_info_sotoon "Added, OK"
return 0
else
_debug_sotoon "Response: $response"
_err_sotoon "Add txt record error."
return 1
fi
fi
_err_sotoon "Add txt record error."
return 1
}
#Remove the txt record after validation.
#Usage: dns_sotoon_rm fulldomain TXT_record
#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_sotoon_rm() {
fulldomain=$1
txtvalue=$2
_info_sotoon "Using Sotoon"
_debug_sotoon fulldomain "$fulldomain"
_debug_sotoon txtvalue "$txtvalue"
Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}"
Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}"
_debug_sotoon "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err_sotoon "invalid domain"
return 1
fi
_debug_sotoon _domain_id "$_domain_id"
_debug_sotoon _sub_domain "$_sub_domain"
_debug_sotoon _domain "$_domain"
_info_sotoon "Removing TXT record"
# First, GET the current domain zone to check for existing TXT records
if ! _sotoon_rest GET "$_domain_id"; then
_err_sotoon "Failed to get domain zone"
return 1
fi
# Check if there are existing TXT records for this subdomain
_existing_txt=""
if _contains "$response" "\"$_sub_domain\""; then
_debug_sotoon "Found existing records for $_sub_domain"
_existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://")
_debug_sotoon "Existing TXT records: $_existing_txt"
fi
# If no existing records, nothing to remove
if [ -z "$_existing_txt" ] || [ "$_existing_txt" = "[]" ] || [ "$_existing_txt" = "null" ]; then
_info_sotoon "No TXT records found, nothing to remove"
return 0
fi
# Remove the specific TXT value from the array
# This handles the case where there are multiple TXT values (wildcard certs)
_remaining_records=$(echo "$_existing_txt" | sed "s/{\"TXT\":\"$txtvalue\"[^}]*},*//g" | sed 's/,]/]/g' | sed 's/\[,/[/g')
_debug_sotoon "Remaining records after removal: $_remaining_records"
# If no records remain, set to null to remove the subdomain entirely
if [ "$_remaining_records" = "[]" ] || [ -z "$_remaining_records" ]; then
_dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":null}}}"
else
_dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_remaining_records}}}"
fi
_debug_sotoon "Remove record payload: $_dns_record"
# Use PATCH to remove the record from the domain zone
if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then
_info_sotoon "Record removed, OK"
return 0
else
_debug_sotoon "Response: $response"
_err_sotoon "Error removing record"
return 1
fi
}
#################### Private functions below ##################################
_get_root() {
domain=$1
i=1
p=1
_debug_sotoon "Getting root domain for: $domain"
_debug_sotoon "Sotoon WorkspaceUUID: $Sotoon_WorkspaceUUID"
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug_sotoon "Checking domain part: $h"
if [ -z "$h" ]; then
#not valid
_err_sotoon "Could not find valid domain"
return 1
fi
_debug_sotoon "Fetching domain zones from Sotoon API"
if ! _sotoon_rest GET ""; then
_err_sotoon "Failed to get domain zones from Sotoon API"
_err_sotoon "Please check your Sotoon_Token, Sotoon_WorkspaceUUID"
return 1
fi
_debug2_sotoon "API Response: $response"
# Check if the response contains our domain
# Sotoon API uses Kubernetes CRD format with spec.origin for domain matching
if _contains "$response" "\"origin\":\"$h\""; then
_debug_sotoon "Found domain by origin: $h"
# In Kubernetes CRD format, the metadata.name is the resource identifier
# The name can be either:
# 1. Same as origin
# 2. Origin with dots replaced by hyphens
# We check both patterns in the response to determine which one exists
# Convert origin to hyphenated version for checking
_h_hyphenated=$(echo "$h" | tr '.' '-')
# Check if the hyphenated name exists in the response
if _contains "$response" "\"name\":\"$_h_hyphenated\""; then
_domain_id="$_h_hyphenated"
_debug_sotoon "Found domain ID (hyphenated): $_domain_id"
# Check if the origin itself is used as name
elif _contains "$response" "\"name\":\"$h\""; then
_domain_id="$h"
_debug_sotoon "Found domain ID (same as origin): $_domain_id"
else
# Fallback: use the hyphenated version (more common)
_domain_id="$_h_hyphenated"
_debug_sotoon "Using hyphenated domain ID as fallback: $_domain_id"
fi
if [ -n "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
_debug_sotoon "Domain ID (metadata.name): $_domain_id"
_debug_sotoon "Sub domain: $_sub_domain"
_debug_sotoon "Domain (origin): $_domain"
return 0
fi
_err_sotoon "Found domain $h but could not extract domain ID"
return 1
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
_sotoon_rest() {
mtd="$1"
resource_id="$2"
data="$3"
token_trimmed=$(echo "$Sotoon_Token" | tr -d '"')
# Construct the API endpoint
_api_path="$SOTOON_API_URL/workspaces/$Sotoon_WorkspaceUUID/domainzones"
if [ -n "$resource_id" ]; then
_api_path="$_api_path/$resource_id"
fi
_debug_sotoon "API Path: $_api_path"
_debug_sotoon "Method: $mtd"
# Set authorization header - Sotoon API uses Bearer token
export _H1="Authorization: Bearer $token_trimmed"
if [ "$mtd" = "GET" ]; then
# GET request
_debug_sotoon "GET" "$_api_path"
response="$(_get "$_api_path")"
elif [ "$mtd" = "PATCH" ]; then
# PATCH Request
export _H2="Content-Type: application/merge-patch+json"
_debug_sotoon data "$data"
response="$(_post "$data" "$_api_path" "" "$mtd")"
else
_err_sotoon "Unknown method: $mtd"
return 1
fi
_debug2_sotoon response "$response"
return 0
}
#Wrappers for logging
_info_sotoon() {
_info "[Sotoon]" "$@"
}
_err_sotoon() {
_err "[Sotoon]" "$@"
}
_debug_sotoon() {
_debug "[Sotoon]" "$@"
}
_debug2_sotoon() {
_debug2 "[Sotoon]" "$@"
}

220
dnsapi/dns_subreg.sh Normal file
View File

@@ -0,0 +1,220 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_subreg_info='Subreg.cz
Site: subreg.cz
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_subreg
Options:
SUBREG_API_USERNAME API username
SUBREG_API_PASSWORD API password
Issues: github.com/acmesh-official/acme.sh/issues/6835
Author: Tomas Pavlic <https://github.com/tomaspavlic>
'
# Subreg SOAP API
# https://subreg.cz/manual/
SUBREG_API_URL="https://soap.subreg.cz/cmd.php"
######## Public functions #####################
# Usage: dns_subreg_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_subreg_add() {
fulldomain=$1
txtvalue=$2
SUBREG_API_USERNAME="${SUBREG_API_USERNAME:-$(_readaccountconf_mutable SUBREG_API_USERNAME)}"
SUBREG_API_PASSWORD="${SUBREG_API_PASSWORD:-$(_readaccountconf_mutable SUBREG_API_PASSWORD)}"
if [ -z "$SUBREG_API_USERNAME" ] || [ -z "$SUBREG_API_PASSWORD" ]; then
_err "SUBREG_API_USERNAME and SUBREG_API_PASSWORD are not set."
return 1
fi
_saveaccountconf_mutable SUBREG_API_USERNAME "$SUBREG_API_USERNAME"
_saveaccountconf_mutable SUBREG_API_PASSWORD "$SUBREG_API_PASSWORD"
if ! _subreg_login; then
return 1
fi
if ! _get_root "$fulldomain"; then
_err "Cannot determine root domain for: $fulldomain"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_subreg_soap "Add_DNS_Record" "<domain>$_domain</domain><record><name>$_sub_domain</name><type>TXT</type><content>$txtvalue</content><prio>0</prio><ttl>120</ttl></record>"
if _subreg_ok; then
_record_id="$(_subreg_map_get record_id)"
if [ -z "$_record_id" ]; then
_err "Subreg API did not return a record_id for TXT record on $fulldomain"
_err "$response"
return 1
fi
_savedomainconf "$(_subreg_record_id_key "$txtvalue")" "$_record_id"
return 0
fi
_err "Failed to add TXT record."
_err "$response"
return 1
}
# Usage: dns_subreg_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_subreg_rm() {
fulldomain=$1
txtvalue=$2
SUBREG_API_USERNAME="${SUBREG_API_USERNAME:-$(_readaccountconf_mutable SUBREG_API_USERNAME)}"
SUBREG_API_PASSWORD="${SUBREG_API_PASSWORD:-$(_readaccountconf_mutable SUBREG_API_PASSWORD)}"
if [ -z "$SUBREG_API_USERNAME" ] || [ -z "$SUBREG_API_PASSWORD" ]; then
_err "SUBREG_API_USERNAME and SUBREG_API_PASSWORD are not set."
return 1
fi
if ! _subreg_login; then
return 1
fi
if ! _get_root "$fulldomain"; then
_err "Cannot determine root domain for: $fulldomain"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_record_id="$(_readdomainconf "$(_subreg_record_id_key "$txtvalue")")"
if [ -z "$_record_id" ]; then
_err "Could not find saved record ID for $fulldomain"
return 1
fi
_debug "Deleting record ID: $_record_id"
_subreg_soap "Delete_DNS_Record" "<domain>$_domain</domain><record><id>$_record_id</id></record>"
if _subreg_ok; then
_cleardomainconf "$(_subreg_record_id_key "$txtvalue")"
return 0
fi
_err "Failed to delete TXT record."
_err "$response"
return 1
}
#################### Private functions #####################
# Build a domain-conf key for storing the record ID of a given TXT value.
# Base64url chars include '-' which is invalid in shell variable names, so replace with '_'.
_subreg_record_id_key() {
printf 'SUBREG_RECORD_ID_%s' "$(printf '%s' "$1" | tr '-' '_')"
}
# Check if the current $response contains a successful status in the ns2:Map format:
# <item><key ...>status</key><value ...>ok</value></item>
_subreg_ok() {
[ "$(_subreg_map_get status)" = "ok" ]
}
# Extract the value for a given key from the ns2:Map response.
# Usage: _subreg_map_get keyname
# Reads from $response
_subreg_map_get() {
_key="$1"
echo "$response" | tr -d '\n\r' | _egrep_o ">${_key}</key><value[^>]*>[^<]*</value>" | sed 's/.*<value[^>]*>//;s/<\/value>//'
}
# Login and store session token in _subreg_ssid
_subreg_login() {
_debug "Logging in to Subreg API as $SUBREG_API_USERNAME"
_subreg_soap_noauth "Login" "<login>$SUBREG_API_USERNAME</login><password>$SUBREG_API_PASSWORD</password>"
if ! _subreg_ok; then
_err "Subreg login failed."
_err "$response"
return 1
fi
_subreg_ssid="$(_subreg_map_get ssid)"
if [ -z "$_subreg_ssid" ]; then
_err "Subreg login: could not extract session token (ssid)."
return 1
fi
_debug "Subreg login: session token (ssid) obtained"
return 0
}
# _get_root _acme-challenge.www.domain.com
# returns _sub_domain and _domain
_get_root() {
domain=$1
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
_err "Unable to retrieve DNS zone matching domain: $domain"
return 1
fi
_subreg_soap "Get_DNS_Zone" "<domain>$h</domain>"
if _subreg_ok; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain="$h"
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
}
# Send a SOAP request without authentication (used for Login)
# _subreg_soap_noauth command inner_xml
_subreg_build_soap() {
_cmd="$1"
_data_inner="$2"
_soap_body="<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"
xmlns:ns1=\"http://soap.subreg.cz/soap\"
xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\"
SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">
<SOAP-ENV:Body>
<ns1:${_cmd}>
<data>
${_data_inner}
</data>
</ns1:${_cmd}>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>"
export _H1="Content-Type: text/xml"
export _H2="SOAPAction: http://soap.subreg.cz/soap#${_cmd}"
response="$(_post "$_soap_body" "$SUBREG_API_URL" "" "POST" "text/xml")"
}
# Send an authenticated SOAP request (requires _subreg_ssid to be set)
# _subreg_soap command inner_xml
_subreg_soap_noauth() {
_cmd="$1"
_inner="$2"
_subreg_build_soap "$_cmd" "$_inner"
}
# Send an authenticated SOAP request (requires _subreg_ssid to be set)
# _subreg_soap command inner_xml
_subreg_soap() {
_cmd="$1"
_inner="$2"
_inner_with_ssid="<ssid>${_subreg_ssid}</ssid>${_inner}"
_subreg_build_soap "$_cmd" "$_inner_with_ssid"
}

View File

@@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_technitium
Options:
Technitium_Server Server Address
Technitium_Token API Token
Technitium_Expiry_Ttl Number of seconds before DNS server auto-deletes the acme record
Issues: github.com/acmesh-official/acme.sh/issues/6116
Author: Henning Reich <acmesh@qupfer.de>
'
@@ -15,7 +16,10 @@ dns_technitium_add() {
_Technitium_account
fulldomain=$1
txtvalue=$2
response="$(_get "$Technitium_Server/api/zones/records/add?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")"
expiryTtl=${Technitium_Expirty_Ttl:-$(_readaccountconf_mutable Technitium_Expiry_Ttl)}
expiryTtl=${expiryTtl:-0}
response="$(_get "$Technitium_Server/api/zones/records/add?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}&expiryTtl=$expiryTtl")"
if _contains "$response" '"status":"ok"'; then
return 0
fi
@@ -28,6 +32,14 @@ dns_technitium_rm() {
_Technitium_account
fulldomain=$1
txtvalue=$2
expiryTtl=${Technitium_Expirty_Ttl:-$(_readaccountconf_mutable Technitium_Expiry_Ttl)}
expiryTtl=${expiryTtl:-0}
if [ "$expiryTtl" -ne 0 ]; then
_info "DNS record is configured to be auto-removed after $expiryTtl seconds. Remove operation is bypassed."
return 0
fi
response="$(_get "$Technitium_Server/api/zones/records/delete?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")"
if _contains "$response" '"status":"ok"'; then
return 0

229
dnsapi/dns_virakcloud.sh Executable file
View File

@@ -0,0 +1,229 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_virakcloud_info='VirakCloud DNS API
Site: VirakCloud.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_virakcloud
Options:
VIRAKCLOUD_API_TOKEN VirakCloud API Bearer Token
'
VIRAKCLOUD_API_URL="https://public-api.virakcloud.com/dns"
######## Public functions #####################
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
#Used to add txt record
dns_virakcloud_add() {
fulldomain=$1
txtvalue=$2
VIRAKCLOUD_API_TOKEN="${VIRAKCLOUD_API_TOKEN:-$(_readaccountconf_mutable VIRAKCLOUD_API_TOKEN)}"
if [ -z "$VIRAKCLOUD_API_TOKEN" ]; then
_err "You haven't configured your VirakCloud API token yet."
_err "Please set VIRAKCLOUD_API_TOKEN environment variable or run:"
_err " export VIRAKCLOUD_API_TOKEN=\"your-api-token\""
return 1
fi
_saveaccountconf_mutable VIRAKCLOUD_API_TOKEN "$VIRAKCLOUD_API_TOKEN"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
if [ "$http_code" = "401" ]; then
return 1
fi
_err "Invalid domain"
return 1
fi
_debug _domain "$_domain"
_debug fulldomain "$fulldomain"
_info "Adding TXT record"
if _virakcloud_rest POST "domains/${_domain}/records" "{\"record\":\"${fulldomain}\",\"type\":\"TXT\",\"ttl\":3600,\"content\":\"${txtvalue}\"}"; then
if echo "$response" | grep -q "success" || echo "$response" | grep -q "\"data\""; then
_info "Added, OK"
return 0
elif echo "$response" | grep -q "already exists" || echo "$response" | grep -q "duplicate"; then
_info "Record already exists, OK"
return 0
else
_err "Add TXT record error."
_err "Response: $response"
return 1
fi
fi
_err "Add TXT record error."
return 1
}
#Usage: fulldomain txtvalue
#Used to remove the txt record after validation
dns_virakcloud_rm() {
fulldomain=$1
txtvalue=$2
VIRAKCLOUD_API_TOKEN="${VIRAKCLOUD_API_TOKEN:-$(_readaccountconf_mutable VIRAKCLOUD_API_TOKEN)}"
if [ -z "$VIRAKCLOUD_API_TOKEN" ]; then
_err "You haven't configured your VirakCloud API token yet."
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
if [ "$http_code" = "401" ]; then
return 1
fi
_err "Invalid domain"
return 1
fi
_debug _domain "$_domain"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
_info "Removing TXT record"
_debug "Getting list of records to find content ID"
if ! _virakcloud_rest GET "domains/${_domain}/records" ""; then
return 1
fi
_debug2 "Records response" "$response"
contentid=""
# Extract innermost objects (content objects) which look like {"id":"...","content_raw":"..."}
# We filter for the one containing txtvalue
target_obj=$(echo "$response" | grep -o '{[^}]*}' | grep "$txtvalue" | _head_n 1)
if [ -n "$target_obj" ]; then
contentid=$(echo "$target_obj" | _egrep_o '"id":"[^"]*"' | cut -d '"' -f 4)
fi
if [ -z "$contentid" ]; then
_debug "Could not find matching record ID in response"
_info "Record not found, may have been already removed"
return 0
fi
_debug contentid "$contentid"
if _virakcloud_rest DELETE "domains/${_domain}/records/${fulldomain}/TXT/${contentid}" ""; then
if echo "$response" | grep -q "success" || [ -z "$response" ]; then
_info "Removed, OK"
return 0
elif echo "$response" | grep -q "not found" || echo "$response" | grep -q "404"; then
_info "Record not found, OK"
return 0
else
_err "Remove TXT record error."
_err "Response: $response"
return 1
fi
fi
_err "Remove TXT record error."
return 1
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _domain=domain.com
_get_root() {
domain=$1
i=1
p=1
# Optimization: skip _acme-challenge subdomain to avoid 422 errors
if echo "$domain" | grep -q "^_acme-challenge."; then
i=2
fi
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
return 1
fi
if ! _virakcloud_rest GET "domains/$h" ""; then
http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
if [ "$http_code" = "401" ]; then
return 1
fi
p=$i
i=$(_math "$i" + 1)
continue
fi
if echo "$response" | grep -q "\"name\""; then
_domain="$h"
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
_virakcloud_rest() {
m=$1
ep="$2"
data="$3"
_debug "$ep"
export _H1="Content-Type: application/json"
export _H2="Authorization: Bearer $VIRAKCLOUD_API_TOKEN"
if [ "$m" != "GET" ]; then
_debug data "$data"
response="$(_post "$data" "$VIRAKCLOUD_API_URL/$ep" "" "$m")"
else
response="$(_get "$VIRAKCLOUD_API_URL/$ep")"
fi
_ret="$?"
if [ "$_ret" != "0" ]; then
_err "error on $m $ep"
return 1
fi
http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug "http response code" "$http_code"
if [ "$http_code" = "401" ]; then
_err "VirakCloud API returned 401 Unauthorized."
_err "Your VIRAKCLOUD_API_TOKEN is invalid or expired."
_err "Please check your API token and try again."
return 1
fi
if [ "$http_code" = "403" ]; then
_err "VirakCloud API returned 403 Forbidden."
_err "Your API token does not have permission to access this resource."
return 1
fi
if [ -n "$http_code" ] && [ "$http_code" -ge 400 ]; then
_err "VirakCloud API error. HTTP code: $http_code"
_err "Response: $response"
return 1
fi
_debug2 response "$response"
return 0
}

View File

@@ -61,7 +61,7 @@ dns_world4you_add() {
if _contains "$res" "successfully"; then
return 0
else
msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "<h3[^>]*>[^<]" | sed 's/<[^>]*>//g' | sed 's/^\s*//g')
msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g')
if [ "$msg" = '' ]; then
_err "Unable to add record: Unknown error"
echo "$ret" >'error-01.html'
@@ -110,7 +110,7 @@ dns_world4you_rm() {
return 3
fi
recordid=$(printf "TXT:%s.:\"%s\"" "$fqdn" "$value" | _base64)
recordid=$(echo "$form" | grep 'data-records="' | sed 's/.*"\([^"]*\)".*/\1/;s/&quot;/"/g;s/},{/}\n{/g' | grep '"type":"TXT"' | grep "\"name\":\"$fqdn\"" | grep "\"value\":\"$value\"" | sed 's/^.*"id":"\([^"]*\)".*$/\1/')
_debug recordid "$recordid"
_resethttp
@@ -125,7 +125,7 @@ dns_world4you_rm() {
if _contains "$res" "successfully"; then
return 0
else
msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "<h3[^>]*>[^<]" | sed 's/<[^>]*>//g' | sed 's/^\s*//g')
msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g')
if [ "$msg" = '' ]; then
_err "Unable to remove record: Unknown error"
echo "$ret" >'error-01.html'

View File

@@ -46,8 +46,8 @@ pushover_send() {
fi
export _H1="Content-Type: application/json"
_content="$(printf "*%s*\n" "$_content" | _json_encode)"
_subject="$(printf "*%s*\n" "$_subject" | _json_encode)"
_content="$(printf "%s" "$_content" | _json_encode)"
_subject="$(printf "%s" "$_subject" | _json_encode)"
_data="{\"token\": \"$PUSHOVER_TOKEN\",\"user\": \"$PUSHOVER_USER\",\"title\": \"$_subject\",\"message\": \"$_content\",\"sound\": \"$PUSHOVER_SOUND\", \"device\": \"$PUSHOVER_DEVICE\", \"priority\": \"$PUSHOVER_PRIORITY\"}"
response="$(_post "$_data" "$PUSHOVER_URI")"

13
notify/telegram.sh Normal file → Executable file
View File

@@ -5,6 +5,10 @@
#TELEGRAM_BOT_APITOKEN=""
#TELEGRAM_BOT_CHATID=""
#TELEGRAM_BOT_URLBASE=""
#TELEGRAM_BOT_THREADID=""
# To get TELEGRAM_BOT_THREADID, just copy the link of the message from the thread.
# https://t.me/c/123456789/XXX/1520 - XXX is the TELEGRAM_BOT_THREADID
telegram_send() {
_subject="$1"
@@ -28,6 +32,12 @@ telegram_send() {
fi
_saveaccountconf_mutable TELEGRAM_BOT_CHATID "$TELEGRAM_BOT_CHATID"
TELEGRAM_BOT_THREADID="${TELEGRAM_BOT_THREADID:-$(_readaccountconf_mutable TELEGRAM_BOT_THREADID)}"
if [ -z "$TELEGRAM_BOT_THREADID" ]; then
TELEGRAM_BOT_THREADID=""
fi
_saveaccountconf_mutable TELEGRAM_BOT_THREADID "$TELEGRAM_BOT_THREADID"
TELEGRAM_BOT_URLBASE="${TELEGRAM_BOT_URLBASE:-$(_readaccountconf_mutable TELEGRAM_BOT_URLBASE)}"
if [ -z "$TELEGRAM_BOT_URLBASE" ]; then
TELEGRAM_BOT_URLBASE="https://api.telegram.org"
@@ -39,6 +49,9 @@ telegram_send() {
_content="$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode)"
_data="{\"text\": \"$_content\", "
_data="$_data\"chat_id\": \"$TELEGRAM_BOT_CHATID\", "
if [ -n "$TELEGRAM_BOT_THREADID" ]; then
_data="$_data\"message_thread_id\": \"$TELEGRAM_BOT_THREADID\", "
fi
_data="$_data\"parse_mode\": \"MarkdownV2\", "
_data="$_data\"disable_web_page_preview\": \"1\"}"