Merge pull request #6895 from acmesh-official/dev

sync
This commit is contained in:
neil
2026-04-04 21:19:05 +08:00
committed by GitHub
5 changed files with 803 additions and 57 deletions

View File

@@ -1,67 +1,181 @@
# GitHub Copilot Shell Scripting (sh) Review Instructions # GitHub Copilot Shell Scripting (sh) Review Instructions for acme.sh
## 🎯 Overall Goal ## Overall Goal
Your role is to act as a rigorous yet helpful senior engineer, reviewing Shell script code (`.sh` files). Ensure the code exhibits the highest levels of robustness, security, and portability. 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. 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 ## Required Output Format
Please adhere to the previous format: organize the feedback into a single, structured report, using the three-level marking system: Organize the feedback into a single, structured report, using the three-level marking system:
1. **🔴 Critical Issues (Must Fix Before Merge)** 1. **Critical Issues (Must Fix Before Merge)**
2. **🟡 Suggestions (Improvements to Consider)** 2. **Suggestions (Improvements to Consider)**
3. **Good Practices (Points to Commend)** 3. **Good Practices (Points to Commend)**
--- ---
## 🔍 Focus Areas and Rules for Shell ## Shell Compatibility
### 1. Robustness and Error Handling - **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`).
* **Shebang:** Check that the script starts with the correct Shebang, must be "#!/usr/bin/env sh". - **Use `return`, never `exit`** -- scripts are sourced, not executed as subprocesses. `exit` would kill the parent shell.
* **Startup Options:** **(🔴 Critical)** Enforce the use of the following combination at the start of the script for safety and robustness: - **Cross-platform**: code must work on Linux, macOS, FreeBSD, Solaris, and BusyBox environments.
* `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.
* **Exit Codes:** Ensure functions and the main script use `exit 0` for success and a non-zero exit code upon failure.
* **Temporary Files:** Check for the use of `mktemp` when creating temporary files to prevent race conditions and security risks.
### 2. Security and Quoting
* **Variable 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**.
* **Hardcoded Secrets:** **(🔴 Critical)** Find and flag any hardcoded passwords, keys, tokens, or authentication details.
* **Untrusted Input:** 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 to using `eval`, as it can lead to arbitrary code execution.
### 3. Readability and Maintainability
* **Function Usage:** Recommend wrapping complex or reusable logic within clearly named functions.
* **Local Variables:** Check that variables inside functions are declared using the `local` keyword to avoid unintentionally modifying global state.
* **Naming Convention:** Variable names should use uppercase letters and underscores (e.g., `MY_VARIABLE`), or follow established project conventions.
* **Test Conditions:** Encourage the use of Bash's **double brackets `[[ ... ]]`** for conditional tests, as it is generally safer and more powerful (e.g., supports pattern matching and avoids Word Splitting) than single brackets `[ ... ]`.
* **Command Substitution:** Encourage using `$(command)` over backticks `` `command` `` for command substitution, as it is easier to nest and improves readability.
### 4. External Commands and Environment
* **`for` Loops:** 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.
* **Use existing acme.sh functions whenever possible.** For example: do not use `tr '[:upper:]' '[:lower:]'`, use `_lower_case` instead.
* **Do not use `head -n`.** Use the `_head_n()` function instead.
* **Do not use `curl` or `wget`.** Use the `_post()` and `_get()` functions instead.
--- ---
### 5. Review Rules for Files Under `dnsapi/`: ## Robustness and Error Handling
* **Each file must contain a `{filename}_add` function** for adding DNS TXT records. It should use `_readaccountconf_mutable` to read the API key and `_saveaccountconf_mutable` to save it. Do not use `_saveaccountconf` or `_readaccountconf`. - **(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.
---
## ❌ Things to Avoid ## Security and Quoting
* Do not comment on purely stylistic issues like spacing or indentation, which should be handled by tools like ShellCheck or Prettier. - **(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**.
* Do not be overly verbose unless a significant issue is found. Keep feedback concise and actionable. - **(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()` |
| `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()` |
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.

33
acme.sh
View File

@@ -5285,7 +5285,7 @@ $_authorizations_map"
_info "Order status is 'ready', let's sleep and retry." _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') _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
_debug "_retryafter" "$_retryafter" _debug "_retryafter" "$_retryafter"
if [ "$_retryafter" ]; then if [ "$_retryafter" ] && [ $_retryafter -gt 0 ]; then
_info "Sleeping for $_retryafter seconds then retrying" _info "Sleeping for $_retryafter seconds then retrying"
_sleep $_retryafter _sleep $_retryafter
else else
@@ -5295,7 +5295,7 @@ $_authorizations_map"
_info "Order status is 'processing', let's sleep and retry." _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') _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
_debug "_retryafter" "$_retryafter" _debug "_retryafter" "$_retryafter"
if [ "$_retryafter" ]; then if [ "$_retryafter" ] && [ $_retryafter -gt 0 ]; then
_info "Sleeping for $_retryafter seconds then retrying" _info "Sleeping for $_retryafter seconds then retrying"
_sleep $_retryafter _sleep $_retryafter
else else
@@ -5555,16 +5555,17 @@ renew() {
. "$DOMAIN_CONF" . "$DOMAIN_CONF"
_debug Le_API "$Le_API" _debug Le_API "$Le_API"
case "$Le_API" in #don't switch it back
"$CA_LETSENCRYPT_V2_TEST") # case "$Le_API" in
_info "Switching back to $CA_LETSENCRYPT_V2" # "$CA_LETSENCRYPT_V2_TEST")
Le_API="$CA_LETSENCRYPT_V2" # _info "Switching back to $CA_LETSENCRYPT_V2"
;; # Le_API="$CA_LETSENCRYPT_V2"
"$CA_GOOGLE_TEST") # ;;
_info "Switching back to $CA_GOOGLE" # "$CA_GOOGLE_TEST")
Le_API="$CA_GOOGLE" # _info "Switching back to $CA_GOOGLE"
;; # Le_API="$CA_GOOGLE"
esac # ;;
# esac
if [ "$_server" ]; then if [ "$_server" ]; then
Le_API="$_server" Le_API="$_server"
@@ -5764,6 +5765,9 @@ ${_skipped_msg}
fi fi
fi fi
if [ "$_TREAT_SKIP_AS_SUCCESS" ] && [ "$_ret" = "$RENEW_SKIP" ]; then
_ret=0
fi
return "$_ret" return "$_ret"
} }
@@ -6982,6 +6986,7 @@ cron() {
_info "Automatically upgraded to: $VER" _info "Automatically upgraded to: $VER"
fi fi
_TREAT_SKIP_AS_SUCCESS="1"
renewAll renewAll
_ret="$?" _ret="$?"
_ACME_IN_CRON="" _ACME_IN_CRON=""
@@ -7229,6 +7234,7 @@ Parameters:
--local-address <ip> Specifies the standalone/tls server listening address, in case you have multiple ip addresses. --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. --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. -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. --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-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. --ca-path <directory> Specifies directory containing CA certificates in PEM format, used by wget or curl.
@@ -7709,6 +7715,9 @@ _process() {
-f | --force) -f | --force)
FORCE="1" FORCE="1"
;; ;;
--treat-skip-as-success | --treatskipassuccess)
_TREAT_SKIP_AS_SUCCESS="1"
;;
--staging | --test) --staging | --test)
STAGE="1" STAGE="1"
;; ;;

202
dnsapi/dns_bh.sh Executable file
View File

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

201
dnsapi/dns_czechia.sh Normal file
View File

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

220
dnsapi/dns_subreg.sh Normal file
View File

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