mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-08-21 15:43:29 +08:00
Compare commits
414 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76d1377fc1 | ||
|
|
15a0f52577 | ||
|
|
fd6ff7b173 | ||
|
|
ed6d1f3447 | ||
|
|
0b2187ab3f | ||
|
|
4b8b23bb90 | ||
|
|
d00b2722ee | ||
|
|
df49dd2ec9 | ||
|
|
e9b0cafac5 | ||
|
|
9882d534af | ||
|
|
539b46adc9 | ||
|
|
f89a9a5de3 | ||
|
|
00090d24b8 | ||
|
|
73a682e561 | ||
|
|
a1b94db94d | ||
|
|
5c94af86f3 | ||
|
|
3843495397 | ||
|
|
a739bf3e3a | ||
|
|
75642a1252 | ||
|
|
668427f285 | ||
|
|
d0e123cb02 | ||
|
|
8587c3e744 | ||
|
|
934870fc77 | ||
|
|
8eea7ca307 | ||
|
|
28f1f07f49 | ||
|
|
ad71a785ec | ||
|
|
d5c8060a65 | ||
|
|
4178c33524 | ||
|
|
044371b00a | ||
|
|
86d98b0461 | ||
|
|
6efd6d5b5a | ||
|
|
454cec6e43 | ||
|
|
afba1455b8 | ||
|
|
f3e61a8ef4 | ||
|
|
0894955895 | ||
|
|
5b5ef91d88 | ||
|
|
618735d11e | ||
|
|
08b2186afe | ||
|
|
3509f6404f | ||
|
|
50dbdd781b | ||
|
|
346acc3f33 | ||
|
|
a79bdb9ef3 | ||
|
|
563415d21f | ||
|
|
c2c5c3cdb7 | ||
|
|
4aeb7bbab0 | ||
|
|
12f639116c | ||
|
|
4cb1c6e1ea | ||
|
|
9aad08ef14 | ||
|
|
3b503a009c | ||
|
|
13d6496653 | ||
|
|
605299947e | ||
|
|
d050f3458b | ||
|
|
d66264e741 | ||
|
|
cf9c70a6c7 | ||
|
|
ef49a9fd23 | ||
|
|
fe5d2e3ef7 | ||
|
|
bf486bb988 | ||
|
|
3c8c735362 | ||
|
|
c397bd6573 | ||
|
|
e26ce2f19c | ||
|
|
af5e592fe4 | ||
|
|
e21be4455f | ||
|
|
5842e6ff4f | ||
|
|
8aea731bd4 | ||
|
|
5d158b1640 | ||
|
|
3198c1af6e | ||
|
|
fc8a61f10f | ||
|
|
d08b4de794 | ||
|
|
cc677ba9f1 | ||
|
|
03860a4978 | ||
|
|
da5e818907 | ||
|
|
f0146bd90e | ||
|
|
20722ea030 | ||
|
|
fdd2e4f19a | ||
|
|
89c86efc56 | ||
|
|
8ca1c83b95 | ||
|
|
8304d3e323 | ||
|
|
b5bfd08e35 | ||
|
|
70f9e255d3 | ||
|
|
12f147bf40 | ||
|
|
bef0fdb1ae | ||
|
|
020a4bb5b3 | ||
|
|
acaaca89ab | ||
|
|
bdfa988b65 | ||
|
|
0e93d2bee1 | ||
|
|
83424e7ba4 | ||
|
|
5bc44caf50 | ||
|
|
47412d1822 | ||
|
|
f68a758974 | ||
|
|
fb4ae10e08 | ||
|
|
824e7b3d80 | ||
|
|
34052e56d7 | ||
|
|
3374c22169 | ||
|
|
4807df0c3e | ||
|
|
b79146281c | ||
|
|
ab3093d58c | ||
|
|
c21db5c5d5 | ||
|
|
ec0dc40ad8 | ||
|
|
7236ba2d7c | ||
|
|
b440e49164 | ||
|
|
80e39eb63c | ||
|
|
61e986f23c | ||
|
|
6a60695549 | ||
|
|
28c088611a | ||
|
|
274fd53c83 | ||
|
|
a4595f334e | ||
|
|
477277bd2d | ||
|
|
d8053ed6b2 | ||
|
|
e44809c180 | ||
|
|
9c245eb37a | ||
|
|
713251c65b | ||
|
|
2e85e6f9bb | ||
|
|
9e584e346d | ||
|
|
163eb1acb9 | ||
|
|
25a3ee48df | ||
|
|
30c9332327 | ||
|
|
dc65223da1 | ||
|
|
01c93b9bbd | ||
|
|
b4ac517c7b | ||
|
|
bfd1f9bf6c | ||
|
|
0cef5edac2 | ||
|
|
8c384a92bf | ||
|
|
ce2d9e9746 | ||
|
|
282b048557 | ||
|
|
f39d066ced | ||
|
|
e2882c536b | ||
|
|
892b3ca219 | ||
|
|
4de00d4603 | ||
|
|
40b29c1879 | ||
|
|
d57ab0ab7d | ||
|
|
d8c062defb | ||
|
|
3e36b61823 | ||
|
|
09009794e0 | ||
|
|
5e670e0d93 | ||
|
|
bc646d120e | ||
|
|
c58da45917 | ||
|
|
b37867d027 | ||
|
|
880d93f7f7 | ||
|
|
5ad2bea129 | ||
|
|
9e51432a6f | ||
|
|
903a53991d | ||
|
|
188aa515ce | ||
|
|
6a9776b1f0 | ||
|
|
2092d6061b | ||
|
|
35f99c545c | ||
|
|
4b278dc1bb | ||
|
|
9bf5179b55 | ||
|
|
ca94221d08 | ||
|
|
70462b5ac3 | ||
|
|
ef035248c3 | ||
|
|
877cbe04c9 | ||
|
|
94c670a759 | ||
|
|
045e4dee2e | ||
|
|
06d9ca61a3 | ||
|
|
dc6a996cc3 | ||
|
|
b08bb2ef69 | ||
|
|
64ac537e72 | ||
|
|
ef2089ceb1 | ||
|
|
2ad984d8ad | ||
|
|
6a37f23b14 | ||
|
|
cf2f9ef251 | ||
|
|
f9ffdbe407 | ||
|
|
778b4a38ed | ||
|
|
397c0605e5 | ||
|
|
b4f30ff026 | ||
|
|
0b66acf332 | ||
|
|
185d92f1e7 | ||
|
|
e031457cfa | ||
|
|
162cfebbbb | ||
|
|
66ef351f36 | ||
|
|
6a98b9f81e | ||
|
|
4219f7b2f6 | ||
|
|
e260b86414 | ||
|
|
21d52b5995 | ||
|
|
e03f8d3ad6 | ||
|
|
6f5a0c5d5e | ||
|
|
57db388932 | ||
|
|
d795cb4850 | ||
|
|
1bd2922bc3 | ||
|
|
47f24126f5 | ||
|
|
76fdac59bc | ||
|
|
49a3d586a3 | ||
|
|
361e7c5ad4 | ||
|
|
3ae9711892 | ||
|
|
fc7168e11d | ||
|
|
260df0048b | ||
|
|
11cae37405 | ||
|
|
61b59831c4 | ||
|
|
96f38655b4 | ||
|
|
ab7835ec58 | ||
|
|
b8b1f1e9b4 | ||
|
|
6b66e734a9 | ||
|
|
4f0a4850a6 | ||
|
|
8a78865174 | ||
|
|
69dd2cf78b | ||
|
|
a961e03a59 | ||
|
|
1eee4dee9c | ||
|
|
1d8788767f | ||
|
|
986a6138eb | ||
|
|
f850e8d0e4 | ||
|
|
d375012c5d | ||
|
|
37c25aa107 | ||
|
|
7b16526e7f | ||
|
|
093f36b4d6 | ||
|
|
e5b47f6402 | ||
|
|
a55d40be97 | ||
|
|
c1c49d5a01 | ||
|
|
88e4d64c1a | ||
|
|
17e0bbcbb6 | ||
|
|
95c7546051 | ||
|
|
c1e17c366f | ||
|
|
88cde7be6d | ||
|
|
c16e059535 | ||
|
|
88d4637ee3 | ||
|
|
74ed0354a3 | ||
|
|
2cc5e66517 | ||
|
|
ba7c368ee5 | ||
|
|
768de270bf | ||
|
|
db1dc4de0d | ||
|
|
fb0926dc81 | ||
|
|
34eb2a655a | ||
|
|
23e1a53ec8 | ||
|
|
67d58a12e7 | ||
|
|
0ed5e21232 | ||
|
|
b2eb1d2bbc | ||
|
|
3c184486c3 | ||
|
|
5a730bf00d | ||
|
|
cbd5dae3b4 | ||
|
|
b9c877adb9 | ||
|
|
d24c7e977e | ||
|
|
6a1ff1c0a6 | ||
|
|
10126410b6 | ||
|
|
f4a575fee1 | ||
|
|
383557df61 | ||
|
|
dba4be8065 | ||
|
|
f1aac43f0f | ||
|
|
94783f46ad | ||
|
|
1d26d4fc91 | ||
|
|
7fc45226da | ||
|
|
a670c07caf | ||
|
|
09cc2bdfa5 | ||
|
|
cc897cab4c | ||
|
|
7ba9597928 | ||
|
|
d2d862420e | ||
|
|
fd6a14de8a | ||
|
|
0eb40c6ce6 | ||
|
|
e321b3c75c | ||
|
|
03d8d3bc1b | ||
|
|
f85de2b0d3 | ||
|
|
5fb42b7339 | ||
|
|
1b2630dc0d | ||
|
|
3fb4c313ec | ||
|
|
65892453be | ||
|
|
cba0ff8321 | ||
|
|
b6523c2301 | ||
|
|
e92d0a7492 | ||
|
|
0e5aab346f | ||
|
|
a5ad15be02 | ||
|
|
27ebf09c5c | ||
|
|
1c65c04b54 | ||
|
|
987882ea37 | ||
|
|
00aaed1b14 | ||
|
|
e3b1bccb6a | ||
|
|
6ca19fb003 | ||
|
|
85ff92170b | ||
|
|
7ac8c6c75b | ||
|
|
6004e7f5cd | ||
|
|
4a7e5d0720 | ||
|
|
b4042d5ccb | ||
|
|
6f66e294de | ||
|
|
5017c12324 | ||
|
|
70bc5a6fba | ||
|
|
3063973744 | ||
|
|
1a2071a120 | ||
|
|
e5dea48d3c | ||
|
|
1413aa332b | ||
|
|
37cf431e80 | ||
|
|
0c9d2dafe3 | ||
|
|
e8708a7489 | ||
|
|
79592c700f | ||
|
|
ad3783170e | ||
|
|
329dab9a67 | ||
|
|
f142f37064 | ||
|
|
0d2955b48d | ||
|
|
95da407de8 | ||
|
|
503ca1e9c2 | ||
|
|
d8722c46d9 | ||
|
|
546c2d47d5 | ||
|
|
2ba615555c | ||
|
|
e94c6be4a1 | ||
|
|
11eaad1fa7 | ||
|
|
daf7f7c268 | ||
|
|
4965c704d7 | ||
|
|
b7fe7a40ba | ||
|
|
5fcca7c7e0 | ||
|
|
b0088c82dc | ||
|
|
875cf056b7 | ||
|
|
3b2c2b16b2 | ||
|
|
45cb36f6d9 | ||
|
|
ed1bd01592 | ||
|
|
a1857af6de | ||
|
|
70e965fd55 | ||
|
|
bee01c938a | ||
|
|
6b6d22c5ba | ||
|
|
67a389cbbf | ||
|
|
d3930639db | ||
|
|
65292b010e | ||
|
|
f39a6fe517 | ||
|
|
2775def93a | ||
|
|
5cbae50ec1 | ||
|
|
51b4fa0080 | ||
|
|
64a6ea68fa | ||
|
|
d97b4477b2 | ||
|
|
b8e394e76a | ||
|
|
671d542898 | ||
|
|
9980ad0fef | ||
|
|
004deaeea1 | ||
|
|
36b8ca2bc0 | ||
|
|
890ab4a7bb | ||
|
|
490b9e2d09 | ||
|
|
ca35e8c118 | ||
|
|
eeb91de6a3 | ||
|
|
657b7195d6 | ||
|
|
054a73f297 | ||
|
|
5c6d8aacbe | ||
|
|
ac0df6bc88 | ||
|
|
f007b46c1b | ||
|
|
c5566eafeb | ||
|
|
75ee17aeeb | ||
|
|
88e9681481 | ||
|
|
20ef8cd369 | ||
|
|
ded539b11c | ||
|
|
5e76ea820c | ||
|
|
90d2ff8fad | ||
|
|
0f42b06b48 | ||
|
|
b500ac3dbb | ||
|
|
894dfdd5d8 | ||
|
|
d0d97a40a6 | ||
|
|
cf5fd403e8 | ||
|
|
72a6a5ce04 | ||
|
|
9381835a7c | ||
|
|
2c9ba9b3df | ||
|
|
9a74c86327 | ||
|
|
692a21ee0d | ||
|
|
ad2cb507a4 | ||
|
|
dca23a98f1 | ||
|
|
68eb6defd3 | ||
|
|
e25e30dcdd | ||
|
|
7ca8a9e449 | ||
|
|
8cb9713493 | ||
|
|
ef76831d37 | ||
|
|
c4671272c0 | ||
|
|
86f2584162 | ||
|
|
020c52e583 | ||
|
|
8713918bdb | ||
|
|
45c4a98f1d | ||
|
|
3252e0ce2e | ||
|
|
eda8614754 | ||
|
|
f1209ce06a | ||
|
|
5808b8d176 | ||
|
|
c2f8b4d1f2 | ||
|
|
014a781426 | ||
|
|
ca08ce4262 | ||
|
|
eb22a84db4 | ||
|
|
d66dd99621 | ||
|
|
5a085f2514 | ||
|
|
e79ee7fb74 | ||
|
|
718ff3a5f5 | ||
|
|
a2e52dadb9 | ||
|
|
ca4cb018d0 | ||
|
|
c2762d3b6f | ||
|
|
947e872850 | ||
|
|
3baa5e145f | ||
|
|
67fd35127c | ||
|
|
c421e2ddfc | ||
|
|
42febe97b5 | ||
|
|
74ca0fb763 | ||
|
|
13631ea2de | ||
|
|
a1eee5923a | ||
|
|
af92bbca2a | ||
|
|
c9287071e3 | ||
|
|
292026288a | ||
|
|
1f056998f3 | ||
|
|
4d7cb7de5f | ||
|
|
67855f21d4 | ||
|
|
8484565e95 | ||
|
|
f7d8abe8ea | ||
|
|
8ca90297e7 | ||
|
|
e08f9080c2 | ||
|
|
419738fbd5 | ||
|
|
7f1423dd6f | ||
|
|
9f09dcd18c | ||
|
|
91081ade3c | ||
|
|
4d933c23a8 | ||
|
|
f29bfd995d | ||
|
|
30d5d1aea9 | ||
|
|
7a0450a7f4 | ||
|
|
5bb09f469f | ||
|
|
90e9d8ff52 | ||
|
|
59a43ce5d1 | ||
|
|
5bc01aa251 | ||
|
|
9eeb979c7b | ||
|
|
eabd7592fe | ||
|
|
e089a3d8a1 | ||
|
|
7560375502 | ||
|
|
1f77b89266 | ||
|
|
7c610124d9 | ||
|
|
a0c5ef4e6f | ||
|
|
218934e767 | ||
|
|
c6a9825c0a | ||
|
|
ee661e5d71 | ||
|
|
5ddffc9172 | ||
|
|
40dd085ef8 | ||
|
|
d5b5bcef56 |
190
.github/copilot-instructions.md
vendored
Normal file
190
.github/copilot-instructions.md
vendored
Normal 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.
|
||||
250
.github/workflows/DNS.yml
vendored
250
.github/workflows/DNS.yml
vendored
@@ -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"
|
||||
|
||||
|
||||
|
||||
|
||||
20
.github/workflows/DragonFlyBSD.yml
vendored
20
.github/workflows/DragonFlyBSD.yml
vendored
@@ -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"
|
||||
|
||||
|
||||
17
.github/workflows/FreeBSD.yml
vendored
17
.github/workflows/FreeBSD.yml
vendored
@@ -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
84
.github/workflows/Haiku.yml
vendored
Normal 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"
|
||||
|
||||
9
.github/workflows/Linux.yml
vendored
9
.github/workflows/Linux.yml
vendored
@@ -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 .. \
|
||||
|
||||
13
.github/workflows/MacOS.yml
vendored
13
.github/workflows/MacOS.yml
vendored
@@ -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
74
.github/workflows/MidnightBSD.yml
vendored
Normal 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"
|
||||
19
.github/workflows/NetBSD.yml
vendored
19
.github/workflows/NetBSD.yml
vendored
@@ -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"
|
||||
|
||||
|
||||
17
.github/workflows/Omnios.yml
vendored
17
.github/workflows/Omnios.yml
vendored
@@ -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"
|
||||
|
||||
|
||||
17
.github/workflows/OpenBSD.yml
vendored
17
.github/workflows/OpenBSD.yml
vendored
@@ -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
80
.github/workflows/OpenIndiana.yml
vendored
Normal 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"
|
||||
|
||||
4
.github/workflows/PebbleStrict.yml
vendored
4
.github/workflows/PebbleStrict.yml
vendored
@@ -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
|
||||
|
||||
21
.github/workflows/Solaris.yml
vendored
21
.github/workflows/Solaris.yml
vendored
@@ -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"
|
||||
|
||||
|
||||
6
.github/workflows/Ubuntu.yml
vendored
6
.github/workflows/Ubuntu.yml
vendored
@@ -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
|
||||
|
||||
13
.github/workflows/Windows.yml
vendored
13
.github/workflows/Windows.yml
vendored
@@ -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/
|
||||
|
||||
4
.github/workflows/dockerhub.yml
vendored
4
.github/workflows/dockerhub.yml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/pr_dns.yml
vendored
1
.github/workflows/pr_dns.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
|
||||
1
.github/workflows/pr_notify.yml
vendored
1
.github/workflows/pr_notify.yml
vendored
@@ -13,6 +13,7 @@ on:
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
|
||||
4
.github/workflows/shellcheck.yml
vendored
4
.github/workflows/shellcheck.yml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/wiki-monitor.yml
vendored
8
.github/workflows/wiki-monitor.yml
vendored
@@ -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:"
|
||||
|
||||
22
Dockerfile
22
Dockerfile
@@ -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
417
README.md
@@ -1,54 +1,73 @@
|
||||
[](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>
|
||||
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)
|
||||
[](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>
|
||||
|
||||

|
||||

|
||||

|
||||
<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>
|
||||
[](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub")
|
||||
[](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|[](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)|OpenBSD
|
||||
|8|[](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)|NetBSD
|
||||
|9|[](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)|DragonFlyBSD
|
||||
|10|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios
|
||||
|11|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian
|
||||
|12|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|CentOS
|
||||
|13|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE
|
||||
|14|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl)
|
||||
|15|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux
|
||||
|16|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora
|
||||
|17|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux
|
||||
|18|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux
|
||||
|19|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia
|
||||
|10|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux
|
||||
|10|[](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml)|MidnightBSD
|
||||
|11|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios
|
||||
|12|[](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml)|OpenIndiana
|
||||
|13|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian
|
||||
|14|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE
|
||||
|15|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl)
|
||||
|16|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux
|
||||
|17|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora
|
||||
|18|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux
|
||||
|19|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux
|
||||
|20|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia
|
||||
|21|[](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://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|[](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
238
acme.sh
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
394
deploy/byteplus_alb.sh
Normal 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
|
||||
}
|
||||
@@ -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
157
deploy/localcopy.sh
Normal 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
276
deploy/multideploy.sh
Normal 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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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
548
dnsapi/dns_baidu.sh
Normal 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
202
dnsapi/dns_bh.sh
Executable 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
373
dnsapi/dns_bhosted.sh
Normal 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
269
dnsapi/dns_cpanel_uapi.sh
Executable 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
|
||||
}
|
||||
@@ -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
201
dnsapi/dns_czechia.sh
Normal 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"
|
||||
}
|
||||
@@ -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
139
dnsapi/dns_efficientip.sh
Executable 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
226
dnsapi/dns_exoscale.sh
Executable file → Normal 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
|
||||
}
|
||||
|
||||
@@ -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
303
dnsapi/dns_gname.sh
Normal 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
501
dnsapi/dns_hostup.sh
Normal 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
244
dnsapi/dns_infoblox_uddi.sh
Normal 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
|
||||
}
|
||||
@@ -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%%\"*}"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
109
dnsapi/dns_mgwm.sh
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
158
dnsapi/dns_opusdns.sh
Executable 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
216
dnsapi/dns_qc.sh
Executable 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
220
dnsapi/dns_sitehost.sh
Executable 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
309
dnsapi/dns_sotoon.sh
Normal 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
220
dnsapi/dns_subreg.sh
Normal 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"
|
||||
}
|
||||
@@ -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
229
dnsapi/dns_virakcloud.sh
Executable 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
|
||||
}
|
||||
@@ -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/"/"/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'
|
||||
|
||||
@@ -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
13
notify/telegram.sh
Normal file → Executable 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\"}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user