Compare commits
@@ -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.
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Apache
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Apache.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Apache.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Apache:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TestingDomain: example.com
|
||||
TEST_ACME_Server: https://localhost:14000/dir
|
||||
HTTPS_INSECURE: 1
|
||||
TEST_LOCAL: 1
|
||||
TEST_CA: "Pebble Intermediate CA"
|
||||
TEST_APACHE: 1
|
||||
CASE: le_test_apache
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y socat apache2
|
||||
- name: Run Pebble
|
||||
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
|
||||
- name: Set up Pebble
|
||||
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
|
||||
- name: Set up Apache
|
||||
# Apache serves on 5002, which is the HTTP-01 validation port in
|
||||
# Pebble's default config; acme.sh appends the challenge Alias to
|
||||
# the main config itself
|
||||
run: |
|
||||
echo "Listen 5002" | sudo tee /etc/apache2/ports.conf
|
||||
sudo sed -i "s/\*:80/*:5002/" /etc/apache2/sites-available/000-default.conf
|
||||
sudo apache2ctl configtest
|
||||
sudo systemctl restart apache2
|
||||
curl -s -o /dev/null -w "%{http_code}" -H "Host: example.com" http://127.0.0.1:5002/ | grep -E "200|403|404"
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
run: cd ../acmetest && sudo --preserve-env ./letest.sh
|
||||
+357
-28
@@ -26,9 +26,9 @@ jobs:
|
||||
id: step_one
|
||||
run: |
|
||||
if [ "${{secrets.TokenName1}}" ] ; then
|
||||
echo "::set-output name=hasToken::true"
|
||||
echo "hasToken=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::set-output name=hasToken::false"
|
||||
echo "hasToken=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Check the value
|
||||
run: echo ${{ steps.step_one.outputs.hasToken }}
|
||||
@@ -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,9 +114,11 @@ jobs:
|
||||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install tools
|
||||
run: brew install socat
|
||||
run: |
|
||||
brew untap aws/tap || true
|
||||
brew install socat
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
@@ -165,7 +167,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
|
||||
@@ -176,9 +178,14 @@ jobs:
|
||||
C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git
|
||||
shell: cmd
|
||||
- name: Set ENV
|
||||
shell: cmd
|
||||
shell: bash
|
||||
run: |
|
||||
echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin >> %GITHUB_ENV%
|
||||
echo 'PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin' >> "$GITHUB_ENV"
|
||||
# cygwin git sees the runner workspace as owned by another user and
|
||||
# fails with "dubious ownership" (exit 128) in the checkout post step
|
||||
echo 'GIT_CONFIG_COUNT=1' >> "$GITHUB_ENV"
|
||||
echo 'GIT_CONFIG_KEY_0=safe.directory' >> "$GITHUB_ENV"
|
||||
echo 'GIT_CONFIG_VALUE_0=*' >> "$GITHUB_ENV"
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
@@ -224,15 +231,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,13 +259,19 @@ 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"
|
||||
|
||||
|
||||
|
||||
|
||||
OpenBSD:
|
||||
GhostBSD:
|
||||
runs-on: ubuntu-latest
|
||||
needs: FreeBSD
|
||||
# GhostBSD VM frequently flakes on boot/ssh; don't let it fail the whole run
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_DNS : ${{ secrets.TEST_DNS }}
|
||||
TestingDomain: ${{ secrets.TestingDomain }}
|
||||
@@ -275,15 +289,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
|
||||
- uses: vmactions/ghostbsd-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
|
||||
prepare: pkg install -y socat curl
|
||||
usesh: true
|
||||
copyback: false
|
||||
sync: nfs
|
||||
run: |
|
||||
if [ "${{ secrets.TokenName1}}" ] ; then
|
||||
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
|
||||
@@ -302,9 +317,69 @@ 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"
|
||||
|
||||
|
||||
|
||||
OpenBSD:
|
||||
runs-on: ubuntu-latest
|
||||
needs: GhostBSD
|
||||
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/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
|
||||
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"
|
||||
|
||||
|
||||
|
||||
NetBSD:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -326,16 +401,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 +430,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 +458,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 +488,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 +576,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 +607,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 +635,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 +662,183 @@ 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"
|
||||
|
||||
|
||||
|
||||
Tribblix:
|
||||
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 Tribblix 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/tribblix-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: zap 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: Tribblix
|
||||
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"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
name: GhostBSD
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/GhostBSD.yml'
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/GhostBSD.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
GhostBSD:
|
||||
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
|
||||
# GhostBSD VM frequently flakes on boot/ssh; don't let it fail the whole run
|
||||
continue-on-error: true
|
||||
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/ghostbsd-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
|
||||
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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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 .. \
|
||||
|
||||
@@ -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 .. \
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Nginx
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Nginx.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Nginx.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Nginx:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TestingDomain: example.com
|
||||
TEST_ACME_Server: https://localhost:14000/dir
|
||||
HTTPS_INSECURE: 1
|
||||
TEST_LOCAL: 1
|
||||
TEST_CA: "Pebble Intermediate CA"
|
||||
TEST_NGINX: 1
|
||||
CASE: le_test_nginx
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y socat nginx
|
||||
- name: Run Pebble
|
||||
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
|
||||
- name: Set up Pebble
|
||||
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
|
||||
- name: Set up nginx
|
||||
# a backend on 8081 plus a site with an aaPanel/BT style
|
||||
# "location ^~ /" proxy block that shadows plain regex locations
|
||||
# (regression for #6125); the site listens on 5002, which is the
|
||||
# HTTP-01 validation port in Pebble's default config
|
||||
run: |
|
||||
sudo tee /etc/nginx/sites-available/default >/dev/null <<'EOF'
|
||||
server {
|
||||
listen 127.0.0.1:8081;
|
||||
location / {
|
||||
default_type text/plain;
|
||||
return 200 "backend";
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 5002 default_server;
|
||||
server_name example.com;
|
||||
location ^~ / {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
sudo nginx -t
|
||||
sudo systemctl restart nginx
|
||||
curl -s -H "Host: example.com" http://127.0.0.1:5002/ | grep backend
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
run: cd ../acmetest && sudo --preserve-env ./letest.sh
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Tribblix
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Tribblix.yml'
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Tribblix.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
Tribblix:
|
||||
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/tribblix-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: zap install 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
name: Blacklist Command
|
||||
|
||||
# An issue titled "blacklist: <login-or-email>" opened by the maintainer
|
||||
# or a write-access member adds that identity to the Blacklist wiki page
|
||||
# (see wiki-guard.yml) and closes the issue. The wiki-monitor notification
|
||||
# embeds a prefilled link that opens such an issue in one click.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
# Share the wiki-guard concurrency group so we never push to the wiki
|
||||
# at the same time as the guard.
|
||||
concurrency:
|
||||
group: wiki-guard
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
blacklist:
|
||||
# Upstream only: forks have no <fork>.wiki repository to push to.
|
||||
if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'blacklist:')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check authorization
|
||||
id: auth
|
||||
run: |
|
||||
assoc="${{ github.event.issue.author_association }}"
|
||||
case "$assoc" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "issue author is not authorized ($assoc); ignoring"
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Checkout wiki repository
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
|
||||
- name: Add the identity to the blacklist page
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
id: add
|
||||
env:
|
||||
TITLE: ${{ github.event.issue.title }}
|
||||
run: |
|
||||
target="$(printf '%s' "$TITLE" \
|
||||
| sed 's/^blacklist:[[:space:]]*//; s/^@//; s/[[:space:]].*$//' \
|
||||
| tr 'A-Z' 'a-z')"
|
||||
case "$target" in
|
||||
''|*[!a-z0-9._+@-]*)
|
||||
echo "invalid target: '$target'"
|
||||
echo "result=invalid" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
echo "target=$target" >> "$GITHUB_OUTPUT"
|
||||
cd wiki
|
||||
if [ ! -e Blacklist.md ]; then
|
||||
echo "result=nopage" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if grep -Fxiq -- "- $target" Blacklist.md; then
|
||||
echo "result=already" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$(tail -c1 Blacklist.md)" ]; then
|
||||
echo >> Blacklist.md
|
||||
fi
|
||||
printf -- '- %s\n' "$target" >> Blacklist.md
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add Blacklist.md
|
||||
git commit -m "blacklist $target (requested in #${{ github.event.issue.number }})"
|
||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||
echo "result=added" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Reply and close
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
RESULT: ${{ steps.add.outputs.result }}
|
||||
TARGET: ${{ steps.add.outputs.target }}
|
||||
with:
|
||||
script: |
|
||||
const result = process.env.RESULT;
|
||||
const target = process.env.TARGET;
|
||||
const messages = {
|
||||
added: `\`${target}\` has been added to the [Blacklist](https://github.com/${context.repo.owner}/${context.repo.repo}/wiki/Blacklist). The wiki guard will revert their recent wiki changes on its next run.`,
|
||||
already: `\`${target}\` is already on the blacklist.`,
|
||||
invalid: "Could not parse a valid login or email from the issue title.",
|
||||
nopage: "The Blacklist wiki page does not exist."
|
||||
};
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: messages[result] || "No action taken."
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: "closed",
|
||||
state_reason: result === "added" ? "completed" : "not_planned"
|
||||
});
|
||||
@@ -41,23 +41,29 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: CheckToken
|
||||
if: "contains(needs.CheckToken.outputs.hasToken, 'true')"
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
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
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- 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
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: login to docker hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
- name: login to ghcr
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
- name: build and push the image
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
@@ -73,6 +79,8 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV"
|
||||
|
||||
DOCKER_LABELS=()
|
||||
while read -r label; do
|
||||
DOCKER_LABELS+=(--label "${label}")
|
||||
@@ -84,3 +92,9 @@ jobs:
|
||||
--output "type=image,push=true" \
|
||||
--build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \
|
||||
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x .
|
||||
- name: mirror the image to ghcr (best-effort)
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \
|
||||
${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
|
||||
|| echo "::warning::GHCR mirror failed; Docker Hub publish unaffected"
|
||||
|
||||
+115
-5
@@ -2,18 +2,128 @@ name: "Update issues"
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
const item = context.payload.issue || context.payload.pull_request;
|
||||
|
||||
// Close on sight anything opened by a user on the wiki Blacklist
|
||||
// page (maintained by the Wiki Guard workflow).
|
||||
let blacklist = [];
|
||||
try {
|
||||
const res = await fetch(`https://raw.githubusercontent.com/wiki/${context.repo.owner}/${context.repo.repo}/Blacklist.md`);
|
||||
if (res.ok) {
|
||||
blacklist = (await res.text()).split("\n")
|
||||
.filter(l => l.startsWith("- "))
|
||||
.map(l => l.slice(2).trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Failed to fetch the blacklist: ${e}`);
|
||||
}
|
||||
// A comment on a closed tracking issue reopens it (the standard
|
||||
// closing note promises this). Bots, blacklisted users and the
|
||||
// maintainer's own comments don't reopen.
|
||||
if (context.eventName === "issue_comment") {
|
||||
const issue = context.payload.issue;
|
||||
const commenter = context.payload.comment.user;
|
||||
if (issue.pull_request || issue.state !== "closed") {
|
||||
return;
|
||||
}
|
||||
if (!/^report\s+(bugs?|issues?)\b/i.test(issue.title)) {
|
||||
return;
|
||||
}
|
||||
if (commenter.type === "Bot" ||
|
||||
commenter.login.toLowerCase() === "neilpang" ||
|
||||
blacklist.includes(commenter.login.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "open"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (blacklist.includes(item.user.login.toLowerCase())) {
|
||||
if (context.payload.pull_request) {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: item.number,
|
||||
state: "closed"
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: item.number,
|
||||
state: "closed",
|
||||
state_reason: "not_planned"
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.payload.pull_request) {
|
||||
return;
|
||||
}
|
||||
|
||||
const issue = context.payload.issue;
|
||||
if (issue.title.startsWith("blacklist:") || issue.title.startsWith("revert:")) {
|
||||
// Handled by the Blacklist / Revert Command workflows.
|
||||
return;
|
||||
}
|
||||
if (/^report\s+(bugs?|issues?)\b/i.test(issue.title)) {
|
||||
// Tracking issue for a third-party dns/deploy/notify api:
|
||||
// no upgrade boilerplate; assign it to the opener, label it,
|
||||
// then close it right away to keep the issue list clean. Any
|
||||
// later comment reopens it (see the issue_comment handler).
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
assignees: [issue.user.login]
|
||||
});
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ["3rd party api"]
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: "Closing this tracking issue for now to keep the issue list clean. It remains the place to report problems with this provider -- if you hit a bug, comment here and the issue will be reopened."
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
state_reason: "completed"
|
||||
});
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you."
|
||||
|
||||
body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you. Before posting the log, review it and REDACT any secrets: private keys (`-----BEGIN ... PRIVATE KEY-----` blocks), API tokens and passwords."
|
||||
})
|
||||
@@ -11,8 +11,9 @@ on:
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
|
||||
@@ -13,8 +13,9 @@ on:
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
name: Revert Command
|
||||
|
||||
# An issue titled "revert: <wiki-commit-sha>" opened by the maintainer or
|
||||
# a write-access member reverts that commit in the wiki repository and
|
||||
# closes the issue. The wiki-monitor notification embeds a prefilled link
|
||||
# that opens such an issue in one click.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
# Share the wiki-guard concurrency group so we never push to the wiki
|
||||
# at the same time as the guard.
|
||||
concurrency:
|
||||
group: wiki-guard
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
revert:
|
||||
# Upstream only: forks have no <fork>.wiki repository to push to.
|
||||
if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'revert:')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check authorization
|
||||
id: auth
|
||||
run: |
|
||||
assoc="${{ github.event.issue.author_association }}"
|
||||
case "$assoc" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "issue author is not authorized ($assoc); ignoring"
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Checkout wiki repository
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Revert the wiki commit
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
id: revert
|
||||
env:
|
||||
TITLE: ${{ github.event.issue.title }}
|
||||
run: |
|
||||
target="$(printf '%s' "$TITLE" \
|
||||
| sed 's/^revert:[[:space:]]*//; s/[[:space:]].*$//' \
|
||||
| tr 'A-Z' 'a-z')"
|
||||
case "$target" in
|
||||
*[!0-9a-f]*|"")
|
||||
echo "invalid commit sha: '$target'"
|
||||
echo "result=invalid" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
echo "target=$target" >> "$GITHUB_OUTPUT"
|
||||
cd wiki
|
||||
if ! git cat-file -e "$target^{commit}" 2>/dev/null; then
|
||||
echo "result=notfound" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
if git revert --no-edit "$target"; then
|
||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||
echo "result=reverted" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
git revert --abort || true
|
||||
echo "result=conflict" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Reply and close
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
RESULT: ${{ steps.revert.outputs.result }}
|
||||
TARGET: ${{ steps.revert.outputs.target }}
|
||||
with:
|
||||
script: |
|
||||
const result = process.env.RESULT;
|
||||
const target = process.env.TARGET;
|
||||
const messages = {
|
||||
reverted: `Wiki commit \`${target}\` has been reverted.`,
|
||||
conflict: `Reverting \`${target}\` conflicts with later edits; please revert manually from the page history.`,
|
||||
notfound: `Commit \`${target}\` was not found in the wiki repository.`,
|
||||
invalid: "Could not parse a commit sha from the issue title."
|
||||
};
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: messages[result] || "No action taken."
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: "closed",
|
||||
state_reason: result === "reverted" ? "completed" : "not_planned"
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
name: Wiki Guard
|
||||
|
||||
# Rules enforced here:
|
||||
# - Only the maintainer and write-access members may delete or rename wiki
|
||||
# pages. Anyone else doing so gets blacklisted and the page restored to
|
||||
# its last good revision.
|
||||
# - Only the maintainer and write-access members may edit the Blacklist
|
||||
# wiki page. Anyone else touching it gets blacklisted and the page
|
||||
# reverted.
|
||||
# - Any wiki change made by a blacklisted identity is reverted.
|
||||
# A "good" revision is one authored by the maintainer, by this bot, or by
|
||||
# a non-blacklisted user -- restoring from the deleted commit's parent is
|
||||
# NOT safe, because vandals replace a page before destroying it and the
|
||||
# parent would launder their version into a bot commit.
|
||||
# The gollum event only fires on page create/update, never on deletion,
|
||||
# so violations are caught by polling the wiki git history.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "*/10 * * * *"
|
||||
gollum:
|
||||
# Piggyback on frequent repo activity, because the cron schedule is
|
||||
# best-effort and often delayed well beyond its interval.
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: wiki-guard
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
guard:
|
||||
# Forks have no <fork>.wiki repository, so the checkout below would
|
||||
# fail there -- run only in the upstream repository.
|
||||
if: github.repository == 'acmesh-official/acme.sh'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout wiki repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Enforce wiki rules
|
||||
id: guard
|
||||
env:
|
||||
# WIKI_GUARD_TOKEN: a PAT with read:org, needed to enumerate
|
||||
# members whose write access comes via the organization -- the
|
||||
# repo-scoped GITHUB_TOKEN only sees direct collaborators.
|
||||
GH_TOKEN: ${{ secrets.WIKI_GUARD_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Logins with write (push) access to the repository, including
|
||||
# organization members -- they may delete/rename pages and edit
|
||||
# the blacklist just like the maintainer. If the API call fails,
|
||||
# the list stays empty and enforcement falls back to
|
||||
# maintainer-only, which is the safe direction.
|
||||
gh api "repos/${GITHUB_REPOSITORY}/collaborators?per_page=100" --paginate \
|
||||
-q '.[] | select(.permissions.push) | .login' 2>/dev/null \
|
||||
| tr 'A-Z' 'a-z' | sort -u > writers.txt || true
|
||||
echo "write-access members loaded: $(wc -l < writers.txt)"
|
||||
cd wiki
|
||||
git config core.quotePath false
|
||||
|
||||
# Any author email under this domain is the maintainer.
|
||||
OWNER_DOMAIN="neilpang.com"
|
||||
# Our own enforcement commits.
|
||||
BOT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com"
|
||||
BL_PAGE="Blacklist.md"
|
||||
# Rolling window; the cron runs every 10 minutes, so 7 days gives
|
||||
# ample overlap without re-judging old changes the maintainer
|
||||
# already accepted.
|
||||
WINDOW="7 days ago"
|
||||
|
||||
: > ../actions.txt
|
||||
: > ../bl_new.txt
|
||||
|
||||
is_owner() {
|
||||
case "$1" in
|
||||
*@"$OWNER_DOMAIN") return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
is_bot() {
|
||||
[ "$1" = "$BOT_EMAIL" ]
|
||||
}
|
||||
|
||||
author_email() {
|
||||
git show -s --format=%ae "$1" | tr 'A-Z' 'a-z'
|
||||
}
|
||||
|
||||
# Identity of a commit author: the GitHub login when the email is a
|
||||
# users.noreply.github.com address, otherwise the email itself.
|
||||
identity_of() {
|
||||
case "$1" in
|
||||
*+*@users.noreply.github.com)
|
||||
printf '%s\n' "$1" | sed 's/^[^+]*+//; s/@users\.noreply\.github\.com$//'
|
||||
;;
|
||||
*@users.noreply.github.com)
|
||||
printf '%s\n' "$1" | sed 's/@users\.noreply\.github\.com$//'
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_blacklisted() {
|
||||
grep -Fxq "$1" ../bl_all.txt
|
||||
}
|
||||
|
||||
# Trusted committers: the maintainer (by email domain), this bot,
|
||||
# and anyone whose GitHub login has write access to the repo.
|
||||
is_trusted() {
|
||||
if is_owner "$1" || is_bot "$1"; then
|
||||
return 0
|
||||
fi
|
||||
grep -Fxq "$(identity_of "$1")" ../writers.txt
|
||||
}
|
||||
|
||||
# Newest commit on file $1 authored by a non-blacklisted user.
|
||||
last_good_for() {
|
||||
for g in $(git log --format=%H --no-renames -- "$1"); do
|
||||
gae="$(author_email "$g")"
|
||||
if is_trusted "$gae"; then
|
||||
printf '%s\n' "$g"
|
||||
return 0
|
||||
fi
|
||||
gid="$(identity_of "$gae")"
|
||||
if ! is_blacklisted "$gid" && ! is_blacklisted "$gae"; then
|
||||
printf '%s\n' "$g"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ -e "$BL_PAGE" ]; then
|
||||
page_existed=1
|
||||
else
|
||||
page_existed=""
|
||||
fi
|
||||
|
||||
# ---- 1. Last good version of the blacklist page: the newest
|
||||
# revision authored by the maintainer or by this bot. Everything
|
||||
# else on that page is tampering and is discarded.
|
||||
bl_good_commit=""
|
||||
for c in $(git log --format=%H --no-renames -- "$BL_PAGE"); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
bl_good_commit="$c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$bl_good_commit" ] && git cat-file -e "$bl_good_commit:$BL_PAGE" 2>/dev/null; then
|
||||
git show "$bl_good_commit:$BL_PAGE" > ../bl_page.txt
|
||||
else
|
||||
{
|
||||
echo "# Blacklist"
|
||||
echo ""
|
||||
echo "Users listed below violated the wiki rules (deleted or renamed"
|
||||
echo "pages, or tampered with this page). Their new issues and pull"
|
||||
echo "requests are closed on sight and their wiki edits are reverted"
|
||||
echo "automatically. Only the maintainer and write-access members"
|
||||
echo "may edit this page."
|
||||
echo ""
|
||||
echo "To pardon a user while their violation is still inside the"
|
||||
echo "scan window, replace their entry with: pardon: username"
|
||||
echo ""
|
||||
} > ../bl_page.txt
|
||||
fi
|
||||
sed -n 's/^- *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_good.txt
|
||||
sed -n 's/^[Pp]ardon: *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_pardon.txt
|
||||
|
||||
bl_add() {
|
||||
if grep -Fxq "$1" ../bl_pardon.txt; then
|
||||
return 0
|
||||
fi
|
||||
if ! grep -Fxq "$1" ../bl_good.txt && ! grep -Fxq "$1" ../bl_new.txt; then
|
||||
printf '%s\n' "$1" >> ../bl_new.txt
|
||||
printf '%s\n' "- blacklisted \`$1\`: $2" >> ../actions.txt
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- 2. Blacklist everyone who deleted or renamed a page.
|
||||
# --no-renames makes a rename count as a deletion of the old path.
|
||||
for c in $(git log --since="$WINDOW" --diff-filter=D --no-renames --format=%H); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
an="$(git show -s --format=%an "$c")"
|
||||
bl_add "$(identity_of "$ae")" "deleted or renamed pages in $c ($an <$ae>)"
|
||||
done
|
||||
|
||||
# ---- 3. Blacklist everyone else who touched the blacklist page.
|
||||
# The revert of their tampering falls out of steps 5 and 6.
|
||||
for c in $(git log --since="$WINDOW" --format=%H --no-renames -- "$BL_PAGE"); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
an="$(git show -s --format=%an "$c")"
|
||||
bl_add "$(identity_of "$ae")" "tampered with \`$BL_PAGE\` in $c ($an <$ae>)"
|
||||
done
|
||||
|
||||
sort -u ../bl_new.txt > ../bl_new_u.txt
|
||||
cat ../bl_good.txt ../bl_new_u.txt | sort -u > ../bl_all.txt
|
||||
|
||||
# ---- 4. Restore pages that are currently missing because a
|
||||
# non-maintainer deleted them, using the last good revision.
|
||||
git log --since="$WINDOW" --diff-filter=D --no-renames --name-only --format= \
|
||||
| sort -u \
|
||||
| while IFS= read -r f; do
|
||||
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ] || [ -e "$f" ]; then
|
||||
continue
|
||||
fi
|
||||
del="$(git log -1 --diff-filter=D --no-renames --format=%H -- "$f")"
|
||||
if [ -z "$del" ]; then
|
||||
continue
|
||||
fi
|
||||
ae="$(author_email "$del")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
good="$(last_good_for "$f")"
|
||||
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
|
||||
git checkout "$good" -- "$f"
|
||||
printf '%s\n' "- restored \`$f\` (deleted in $del) from its last good revision $good" >> ../actions.txt
|
||||
fi
|
||||
done
|
||||
|
||||
# ---- 5. Revert every recent change made by a blacklisted
|
||||
# identity: each touched file goes back to its newest revision
|
||||
# authored by a non-blacklisted user; a file that has no such
|
||||
# revision (they created it) is removed.
|
||||
if [ -s ../bl_all.txt ]; then
|
||||
for c in $(git log --since="$WINDOW" --format=%H --no-renames); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
id="$(identity_of "$ae")"
|
||||
if ! is_blacklisted "$id" && ! is_blacklisted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
git show --name-only --no-renames --format= "$c" \
|
||||
| while IFS= read -r f; do
|
||||
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ]; then
|
||||
continue
|
||||
fi
|
||||
good="$(last_good_for "$f")"
|
||||
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
|
||||
want="$(git rev-parse "$good:$f")"
|
||||
have="$(git hash-object -- "$f" 2>/dev/null || echo missing)"
|
||||
if [ "$want" != "$have" ]; then
|
||||
git checkout "$good" -- "$f"
|
||||
printf '%s\n' "- reverted \`$f\` to its last good revision $good (undoing change by \`$id\` in $c)" >> ../actions.txt
|
||||
fi
|
||||
elif [ -e "$f" ]; then
|
||||
git rm -q -- "$f"
|
||||
printf '%s\n' "- removed \`$f\` created by blacklisted \`$id\` in $c" >> ../actions.txt
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
# ---- 6. Regenerate the blacklist page: the last good text plus
|
||||
# any newly blacklisted identities. This both reverts tampering
|
||||
# and records new violators; manual edits by the maintainer are
|
||||
# preserved as the new good text.
|
||||
cp ../bl_page.txt ../bl_page_new.txt
|
||||
if [ -s ../bl_page_new.txt ] && [ -n "$(tail -c1 ../bl_page_new.txt)" ]; then
|
||||
echo >> ../bl_page_new.txt
|
||||
fi
|
||||
while IFS= read -r id; do
|
||||
if [ -n "$id" ] && ! grep -Fxiq -- "- $id" ../bl_page_new.txt; then
|
||||
printf -- '- %s\n' "$id" >> ../bl_page_new.txt
|
||||
fi
|
||||
done < ../bl_new_u.txt
|
||||
if ! cmp -s ../bl_page_new.txt "$BL_PAGE" 2>/dev/null; then
|
||||
cp ../bl_page_new.txt "$BL_PAGE"
|
||||
git add -- "$BL_PAGE"
|
||||
if [ -n "$page_existed" ] || [ -s ../bl_new_u.txt ]; then
|
||||
printf '%s\n' "- updated \`$BL_PAGE\`" >> ../actions.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 7. Commit, push, notify.
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "$BOT_EMAIL"
|
||||
git commit -m "wiki-guard: restore pages and enforce blacklist"
|
||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||
fi
|
||||
if [ -s ../actions.txt ]; then
|
||||
{
|
||||
echo "The wiki guard handled the following rule violations:"
|
||||
echo ""
|
||||
cat ../actions.txt
|
||||
echo ""
|
||||
echo "Blacklist: https://github.com/${GITHUB_REPOSITORY}/wiki/Blacklist"
|
||||
echo "Wiki: https://github.com/${GITHUB_REPOSITORY}/wiki"
|
||||
} > ../guard-msg.txt
|
||||
echo "acted=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No rule violations found."
|
||||
echo "acted=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create issue to notify Neilpang
|
||||
if: steps.guard.outputs.acted == 'true'
|
||||
uses: peter-evans/create-issue-from-file@v6
|
||||
with:
|
||||
title: "Wiki guard: rule violations handled"
|
||||
content-filepath: ./guard-msg.txt
|
||||
assignees: Neilpang
|
||||
@@ -6,15 +6,17 @@ on:
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- name: Checkout wiki repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate wiki change message
|
||||
id: msg
|
||||
run: |
|
||||
actor="${{ github.actor }}"
|
||||
sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH")
|
||||
@@ -22,9 +24,21 @@ 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
|
||||
# Skip notification when the change was authored by the
|
||||
# maintainer himself (any author email under neilpang.com),
|
||||
# e.g. a direct git push to the wiki repository.
|
||||
author_email=$(git show -s --format=%ae "$page_sha" 2>/dev/null | tr 'A-Z' 'a-z')
|
||||
case "$author_email" in
|
||||
*@neilpang.com)
|
||||
echo "Change authored by maintainer ($author_email); skipping notification."
|
||||
echo "notify=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1)
|
||||
if [ -n "$prev_sha" ]; then
|
||||
git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff
|
||||
@@ -35,19 +49,25 @@ 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](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=revert%3A+${page_sha}&body=Revert+wiki+commit+${page_sha}+by+@${actor}.)"
|
||||
echo ""
|
||||
echo "[Click here to Blacklist @$actor](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=blacklist%3A+${actor}&body=Blacklist+@${actor},+requested+from+the+wiki+monitor.)"
|
||||
echo ""
|
||||
echo "----"
|
||||
echo "### diff:"
|
||||
echo "### diff:"
|
||||
echo '```diff'
|
||||
cat wiki.diff
|
||||
echo '```'
|
||||
} > wiki-change-msg.txt
|
||||
echo "notify=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create issue to notify Neilpang
|
||||
uses: peter-evans/create-issue-from-file@v5
|
||||
if: steps.msg.outputs.notify == 'true'
|
||||
uses: peter-evans/create-issue-from-file@v6
|
||||
with:
|
||||
title: "Wiki edited"
|
||||
content-filepath: ./wiki-change-msg.txt
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Contributing
|
||||
|
||||
1. Do NOT send pull request to `master` branch.
|
||||
Please send to `dev` branch instead.
|
||||
Any PR to `master` branch will NOT be merged.
|
||||
|
||||
2. For dns api support, read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
|
||||
You will NOT get any review without passing this guide. You also need to fix the CI errors.
|
||||
+18
-4
@@ -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 % 6)) \n \
|
||||
echo \"\$random_minute \$random_hour,\$((\$random_hour + 6)),\$((\$random_hour + 12)),\$((\$random_hour + 18)) * * * \\\"\$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
|
||||
|
||||
@@ -1,54 +1,89 @@
|
||||
[](https://zerossl.com/?fromacme.sh)
|
||||
<p align="center">
|
||||
<a href="https://zerossl.com?utm_source=acme-sh">
|
||||
<picture>
|
||||
<!-- Dark mode -->
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://github.com/user-attachments/assets/1308516b-e0cc-496d-b5df-e3932423ead6" />
|
||||
<!-- Light mode -->
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://github.com/user-attachments/assets/4ba7a79e-8cc9-4d49-87fc-02d44fb7b043" />
|
||||
<!-- Fallback for environments without media queries -->
|
||||
<img
|
||||
alt="ZeroSSL"
|
||||
src="https://github.com/user-attachments/assets/4ba7a79e-8cc9-4d49-87fc-02d44fb7b043"
|
||||
height="auto" />
|
||||
</picture>
|
||||
</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/GhostBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg" alt="GhostBSD"></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/Tribblix.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg" alt="Tribblix"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg" alt="Haiku"></a>
|
||||
</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 +97,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 +112,81 @@ 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
|
||||
|26|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
|
||||
|27|[](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
|
||||
|
||||
|
||||
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 |
|
||||
| 📌 [DNS persist mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode) | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) |
|
||||
|
||||
# 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 +196,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 +213,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 +260,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 +283,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 +297,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 +409,154 @@ 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)
|
||||
### 🔟 Use DNS Persist Mode
|
||||
|
||||
Just set the `keylength` to a valid, supported, value.
|
||||
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode
|
||||
|
||||
Valid values for the `keylength` parameter are:
|
||||
📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)
|
||||
|
||||
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)**
|
||||
DNS persist mode lets you place a **single, long‑lived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals.
|
||||
|
||||
For example:
|
||||
#### 🪄 Step 1: Print the TXT record value
|
||||
|
||||
### Single domain with ECDSA P-384 certificate
|
||||
```bash
|
||||
acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] [--dns-persist-days 365]
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--server <ca>` | Pick the CA (default is your configured default). The account is registered automatically if you have not used this CA before. |
|
||||
| `--dns-persist-wildcard` | Adds `policy=wildcard` to the record so it also authorizes wildcard / subdomain certs. |
|
||||
| `--dns-persist-ca-name <name>` | Use a specific CA identity domain (e.g. `sectigo.com`). If omitted, identities are read from the ACME directory's `caaIdentities` field and one record per identity is printed — you only need to add **any one** of them. |
|
||||
| `--dns-persist-days <N>` | Adds `persistUntil=<unix-timestamp>` to the record, set to N days from now. The CA will refuse new validations against the record after that time. Omit for a record with no expiry. |
|
||||
|
||||
You should get an output like:
|
||||
|
||||
```sh
|
||||
TXT persist domain:_validation-persist.example.com
|
||||
TXT persist value :"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789"
|
||||
```
|
||||
|
||||
#### ✍️ Step 2: Add the TXT record to your DNS
|
||||
|
||||
Add the printed `TXT persist domain` / `TXT persist value` pair as a TXT record at your DNS provider, then wait for it to propagate.
|
||||
|
||||
#### 📜 Step 3: Issue the certificate
|
||||
|
||||
```bash
|
||||
acme.sh --issue -d example.com --dns-persist
|
||||
```
|
||||
|
||||
✅ **Done!** No challenge token is provisioned during issuance — the CA reads the persistent TXT record directly.
|
||||
|
||||
> 🔄 Renewals just work: `acme.sh --renew -d example.com` (or the cron job) reuses the same TXT record automatically — no further DNS edits needed.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣1️⃣ Issue Certificates of Different Key Types (ECC or RSA)
|
||||
|
||||
Just set the `keylength` to a valid, supported value.
|
||||
|
||||
**Valid values for the `keylength` parameter:**
|
||||
|
||||
| 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 |
|
||||
|
||||
**Examples:**
|
||||
|
||||
#### 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️⃣2️⃣ 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️⃣3️⃣ 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, **or earlier when the CA's ARI says so** (see below).
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
#### 📡 ACME Renewal Information (ARI) — RFC 9773
|
||||
|
||||
# 13. How to stop cert renewal
|
||||
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/ARI
|
||||
|
||||
If the CA exposes a `renewalInfo` endpoint in its ACME directory (Let's Encrypt, ZeroSSL, etc.), `acme.sh` follows [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html) automatically — **no flag needed, no opt-in**:
|
||||
|
||||
| What | When | Why |
|
||||
|------|------|-----|
|
||||
| 🔍 **Polls `suggestedWindow`** | Every cron run, before deciding to skip | Lets the CA shift the renewal time forward in case of an incident (key compromise, mass revocation, etc.) |
|
||||
| 🎯 **Picks a random renewal time** inside the window | Right after a successful issuance/renewal | Disperses renewals across the network so all clients don't hit the CA at the same instant |
|
||||
| 🔗 **Sends `replaces=<certID>`** in `newOrder` | On renewal | Lets the CA correlate the new order with the certificate it supersedes (RFC 9773 §5) |
|
||||
| ↩️ **Retries without `replaces`** | If the CA rejects with `alreadyReplaced` or an ARI validation error | Robust against edge cases (e.g. switching CAs, retired issuers) |
|
||||
|
||||
**Renewal trigger logic:** the cert is renewed if **any one** of the following becomes true:
|
||||
|
||||
1. `--force` is given
|
||||
2. The CA's **ARI `suggestedWindow` has started**
|
||||
3. The cached `Le_NextRenewTime` has passed (default fallback for CAs without ARI)
|
||||
|
||||
You can see the resulting next renewal time (already ARI-picked when applicable) in:
|
||||
|
||||
```sh
|
||||
acme.sh --info -d example.com
|
||||
# Look for: Le_NextRenewTimeStr=...
|
||||
```
|
||||
|
||||
For the live ARI window the CA is currently advertising, run with `--debug 2`:
|
||||
|
||||
```sh
|
||||
acme.sh --renew -d example.com --debug 2 2>&1 | grep -i 'ARI suggestedWindow'
|
||||
```
|
||||
|
||||
> 💡 If your CA does not advertise `renewalInfo`, `acme.sh` falls back to the classic 30-day rule — no behavior change.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣4️⃣ 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 +566,80 @@ 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️⃣5️⃣ 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️⃣6️⃣ 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️⃣7️⃣ 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️⃣8️⃣ 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️⃣9️⃣ 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
|
||||
If you want to become a contributor make sure to read [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
### 💰 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 +654,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>
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣0️⃣ 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️⃣1️⃣ 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️⃣2️⃣ 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 +686,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">
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
# Bash completion for acme.sh: https://github.com/acmesh-official/acme.sh
|
||||
#
|
||||
# "acme.sh --install" copies this file to the acme.sh home dir and wires
|
||||
# it into acme.sh.env, so the completion is loaded automatically in new
|
||||
# bash sessions after installation.
|
||||
#
|
||||
# To use it without installing acme.sh, source it from ~/.bashrc, or copy
|
||||
# it to /usr/share/bash-completion/completions/acme.sh
|
||||
#
|
||||
# Zsh users can load it with:
|
||||
# autoload -U +X bashcompinit && bashcompinit
|
||||
# . /path/to/acme.sh.completion
|
||||
|
||||
# This file may also be sourced by non-bash shells via acme.sh.env,
|
||||
# so silently do nothing if the "complete" builtin is not available.
|
||||
if ! command -v complete >/dev/null 2>&1; then
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
# Add each word of $1 that starts with $cur to COMPREPLY.
|
||||
# The words are read line by line, so that candidates like a wildcard
|
||||
# domain "*.example.com" are never glob-expanded against the cwd.
|
||||
_acme_sh_add_matches() {
|
||||
local _word
|
||||
while read -r _word; do
|
||||
[ -n "$_word" ] || continue
|
||||
case "$_word" in
|
||||
"$cur"*) COMPREPLY=("${COMPREPLY[@]}" "$_word") ;;
|
||||
esac
|
||||
done <<EOF
|
||||
$(printf '%s\n' "$1" | tr ' ' '\n')
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
_acme_sh_files() {
|
||||
local _file
|
||||
while IFS= read -r _file; do
|
||||
[ -n "$_file" ] || continue
|
||||
COMPREPLY=("${COMPREPLY[@]}" "$_file")
|
||||
done <<EOF
|
||||
$(compgen -f -- "$cur")
|
||||
EOF
|
||||
if command -v compopt >/dev/null 2>&1; then
|
||||
compopt -o filenames 2>/dev/null
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
_acme_sh_dirs() {
|
||||
local _dir
|
||||
while IFS= read -r _dir; do
|
||||
[ -n "$_dir" ] || continue
|
||||
COMPREPLY=("${COMPREPLY[@]}" "$_dir")
|
||||
done <<EOF
|
||||
$(compgen -d -- "$cur")
|
||||
EOF
|
||||
if command -v compopt >/dev/null 2>&1; then
|
||||
compopt -o filenames 2>/dev/null
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Complete the domains that already have a cert: every directory in the
|
||||
# config home that contains a "<domain>.conf" file ("_ecc" suffix stripped).
|
||||
_acme_sh_domains() {
|
||||
local _dir _name _domains=""
|
||||
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
|
||||
for _dir in "$_acme_conf_home"/*/; do
|
||||
[ -d "$_dir" ] || continue
|
||||
_name="${_dir%/}"
|
||||
_name="${_name##*/}"
|
||||
_name="${_name%_ecc}"
|
||||
if [ -f "${_dir}${_name}.conf" ]; then
|
||||
case " $_domains " in
|
||||
*" $_name "*) ;;
|
||||
*) _domains="$_domains $_name" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
_acme_sh_add_matches "$_domains"
|
||||
}
|
||||
|
||||
# Complete hook names from a subfolder of the acme.sh home dir.
|
||||
# $1: subfolder (dnsapi/deploy/notify), $2: file name prefix or empty.
|
||||
_acme_sh_hooks() {
|
||||
local _file _hooks=""
|
||||
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
|
||||
for _file in "$_acme_home/$1/$2"*.sh; do
|
||||
[ -f "$_file" ] || continue
|
||||
_file="${_file##*/}"
|
||||
_hooks="$_hooks ${_file%.sh}"
|
||||
done
|
||||
_acme_sh_add_matches "$_hooks"
|
||||
}
|
||||
|
||||
_acme_sh_completion() {
|
||||
local cur prev _acme_home _acme_conf_home
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev=""
|
||||
if [ "$COMP_CWORD" -gt 0 ]; then
|
||||
prev="${COMP_WORDS[COMP_CWORD - 1]}"
|
||||
fi
|
||||
_acme_home="${LE_WORKING_DIR:-$HOME/.acme.sh}"
|
||||
_acme_conf_home="${LE_CONFIG_HOME:-$_acme_home}"
|
||||
|
||||
# The first argument is the command.
|
||||
if [ "$COMP_CWORD" -eq 1 ]; then
|
||||
_acme_sh_add_matches "
|
||||
--help
|
||||
--version
|
||||
--install
|
||||
--install-online
|
||||
--uninstall
|
||||
--upgrade
|
||||
--issue
|
||||
--deploy
|
||||
--sign-csr
|
||||
--show-csr
|
||||
--install-cert
|
||||
--renew
|
||||
--renew-all
|
||||
--revoke
|
||||
--remove
|
||||
--list
|
||||
--list-profiles
|
||||
--info
|
||||
--to-pkcs12
|
||||
--to-pkcs8
|
||||
--create-account-key
|
||||
--create-domain-key
|
||||
--create-csr
|
||||
--deactivate
|
||||
--update-account
|
||||
--register-account
|
||||
--deactivate-account
|
||||
--make-dns-persist-value
|
||||
--install-cronjob
|
||||
--uninstall-cronjob
|
||||
--cron
|
||||
--set-notify
|
||||
--set-default-ca
|
||||
--set-default-chain
|
||||
"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Complete the value of the previous option.
|
||||
case "$prev" in
|
||||
-d | --domain | --challenge-alias | --domain-alias)
|
||||
_acme_sh_domains
|
||||
return 0
|
||||
;;
|
||||
--dns)
|
||||
# The dns hook argument is optional, keep completing options if the
|
||||
# current word already looks like one.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_hooks "dnsapi" "dns_"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--deploy-hook)
|
||||
_acme_sh_hooks "deploy" ""
|
||||
return 0
|
||||
;;
|
||||
--notify-hook)
|
||||
_acme_sh_hooks "notify" ""
|
||||
return 0
|
||||
;;
|
||||
--server)
|
||||
_acme_sh_add_matches "letsencrypt letsencrypt_test zerossl sslcom google google_test actalis"
|
||||
return 0
|
||||
;;
|
||||
-k | --keylength | -ak | --accountkeylength)
|
||||
_acme_sh_add_matches "2048 3072 4096 8192 ec-256 ec-384 ec-521"
|
||||
return 0
|
||||
;;
|
||||
--debug)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_add_matches "0 1 2 3"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--log)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_files
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--nginx)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_files
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--auto-upgrade | --always-force-new-domain-key)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_add_matches "0 1"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--log-level)
|
||||
_acme_sh_add_matches "1 2"
|
||||
return 0
|
||||
;;
|
||||
--syslog)
|
||||
_acme_sh_add_matches "0 3 6 7"
|
||||
return 0
|
||||
;;
|
||||
--notify-level)
|
||||
_acme_sh_add_matches "0 1 2 3"
|
||||
return 0
|
||||
;;
|
||||
--notify-mode)
|
||||
_acme_sh_add_matches "0 1"
|
||||
return 0
|
||||
;;
|
||||
--revoke-reason)
|
||||
_acme_sh_add_matches "0 1 2 3 4 5 6 7 8 9 10"
|
||||
return 0
|
||||
;;
|
||||
--cert-file | --key-file | --ca-file | --fullchain-file | --csr | --accountconf | --accountkey | --ca-bundle | --openssl-bin)
|
||||
_acme_sh_files
|
||||
return 0
|
||||
;;
|
||||
-w | --webroot | --home | --cert-home | --config-home | --ca-path)
|
||||
_acme_sh_dirs
|
||||
return 0
|
||||
;;
|
||||
-m | --email | --password | --useragent | --days | --valid-from | --valid-to | --httpport | --tlsport | --local-address | --dnssleep | --pre-hook | --post-hook | --renew-hook | --reloadcmd | --extended-key-usage | -b | --branch | --notify-source | --eab-kid | --eab-hmac-key | --preferred-chain | --cert-profile | --certificate-profile | --dns-persist-ca-name | --dns-persist-days)
|
||||
# These options take a free-form value, offer nothing.
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Complete the parameters.
|
||||
_acme_sh_add_matches "
|
||||
--accountconf
|
||||
--accountkey
|
||||
--accountkeylength
|
||||
--alpn
|
||||
--always-force-new-domain-key
|
||||
--apache
|
||||
--auto-upgrade
|
||||
--branch
|
||||
--ca-bundle
|
||||
--ca-file
|
||||
--ca-path
|
||||
--cert-file
|
||||
--cert-home
|
||||
--cert-profile
|
||||
--challenge-alias
|
||||
--config-home
|
||||
--csr
|
||||
--days
|
||||
--debug
|
||||
--deploy-hook
|
||||
--dns
|
||||
--dns-persist
|
||||
--dns-persist-ca-name
|
||||
--dns-persist-days
|
||||
--dns-persist-wildcard
|
||||
--dnssleep
|
||||
--domain
|
||||
--domain-alias
|
||||
--eab-hmac-key
|
||||
--eab-kid
|
||||
--ecc
|
||||
--email
|
||||
--extended-key-usage
|
||||
--force
|
||||
--force-color
|
||||
--fullchain-file
|
||||
--home
|
||||
--httpport
|
||||
--insecure
|
||||
--key-file
|
||||
--keylength
|
||||
--listen-v4
|
||||
--listen-v6
|
||||
--listraw
|
||||
--local-address
|
||||
--log
|
||||
--log-level
|
||||
--nginx
|
||||
--no-color
|
||||
--no-cron
|
||||
--no-profile
|
||||
--notify-hook
|
||||
--notify-level
|
||||
--notify-mode
|
||||
--notify-source
|
||||
--ocsp-must-staple
|
||||
--openssl-bin
|
||||
--output-insecure
|
||||
--password
|
||||
--post-hook
|
||||
--pre-hook
|
||||
--preferred-chain
|
||||
--reloadcmd
|
||||
--renew-hook
|
||||
--revoke-reason
|
||||
--server
|
||||
--staging
|
||||
--standalone
|
||||
--stateless
|
||||
--stop-renew-on-error
|
||||
--syslog
|
||||
--tlsport
|
||||
--treat-skip-as-success
|
||||
--use-wget
|
||||
--useragent
|
||||
--valid-from
|
||||
--valid-to
|
||||
--webroot
|
||||
--yes-I-know-dns-manual-mode-enough-go-ahead-please
|
||||
"
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _acme_sh_completion acme.sh
|
||||
+1
-1
@@ -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'
|
||||
}
|
||||
|
||||
+1
-1
@@ -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'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034,SC2154
|
||||
|
||||
# Deploy hook: Baidu Cloud CDN
|
||||
#
|
||||
# Code generated by GitHub Copilot with Claude Sonnet 4.6 and OpenAI Codex with GPT-5.6 Sol
|
||||
#
|
||||
# API Doc: https://cloud.baidu.com/doc/CDN/s/Zkna2r57w
|
||||
#
|
||||
# Uses the same credential variables as dnsapi/dns_baidu.sh:
|
||||
# export Baidu_AK="your-access-key-id"
|
||||
# export Baidu_SK="your-secret-access-key"
|
||||
#
|
||||
# To deploy to a CDN domain different from the certificate CN
|
||||
# (e.g. wildcard or multi-domain certs):
|
||||
# export DEPLOY_BAIDU_CDN_DOMAIN="cdn.example.com"
|
||||
#
|
||||
# Multiple CDN domains sharing the same certificate:
|
||||
# export DEPLOY_BAIDU_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
|
||||
|
||||
BAIDU_CDN_HOST="cdn.baidubce.com"
|
||||
_BAIDU_CDN_BCE_AUTH_RESULT=""
|
||||
|
||||
baidu_cdn_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"
|
||||
|
||||
if ! _baidu_cdn_load_credentials; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_BAIDU_CDN_DOMAIN
|
||||
if [ "$DEPLOY_BAIDU_CDN_DOMAIN" ]; then
|
||||
_savedeployconf DEPLOY_BAIDU_CDN_DOMAIN "$DEPLOY_BAIDU_CDN_DOMAIN"
|
||||
else
|
||||
DEPLOY_BAIDU_CDN_DOMAIN="$_cdomain"
|
||||
fi
|
||||
|
||||
# Build JSON "domains" array from space-separated domain list
|
||||
_domains_json=""
|
||||
for _d in $DEPLOY_BAIDU_CDN_DOMAIN; do
|
||||
_d_e="$(_baidu_cdn_json_escape "$_d")"
|
||||
if [ -z "$_domains_json" ]; then
|
||||
_domains_json="\"${_d_e}\""
|
||||
else
|
||||
_domains_json="${_domains_json},\"${_d_e}\""
|
||||
fi
|
||||
done
|
||||
|
||||
# Build a valid cert name: must start with a letter, allow [A-Za-z0-9-/.], max 65 chars
|
||||
_cert_name="$(printf "%s" "$_cdomain" | sed 's/\*\./wildcard./g;s/[^A-Za-z0-9./]/-/g' | cut -c 1-65)"
|
||||
case "$_cert_name" in
|
||||
[A-Za-z]*) ;;
|
||||
*) _cert_name="c${_cert_name}" ;;
|
||||
esac
|
||||
|
||||
# PEM content is already Base64 inside the -----BEGIN/END----- wrappers.
|
||||
# The API expects the raw PEM as a JSON string, so newlines must be escaped as \n.
|
||||
_cert_pem="$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')"
|
||||
_key_pem="$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')"
|
||||
|
||||
_debug2 _cert_name "$_cert_name"
|
||||
_debug2 _domains_json "[$_domains_json]"
|
||||
|
||||
# Build JSON payload
|
||||
_payload="{\"domains\":[${_domains_json}],\"certificate\":{\"certName\":\"${_cert_name}\",\"certServerData\":\"${_cert_pem}\",\"certPrivateData\":\"${_key_pem}\"}}"
|
||||
|
||||
# Generate BCE v1 authorization header (query string included in canonical request)
|
||||
_cdn_path="/v2/domain/certificate"
|
||||
_cdn_query="action=put"
|
||||
_ts="$(_utc_date | sed 's/ /T/')Z"
|
||||
_content_type="application/json; charset=utf-8"
|
||||
_payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)"
|
||||
|
||||
if ! _baidu_cdn_bce_auth "POST" "$_cdn_path" "$_cdn_query" "$BAIDU_CDN_HOST" "$_ts" "3600" "$_content_type" "$_payload_hash"; then
|
||||
_err "Failed to sign request"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_H1="Authorization: $_BAIDU_CDN_BCE_AUTH_RESULT"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_CDN_HOST"
|
||||
_H5=""
|
||||
|
||||
_url="https://${BAIDU_CDN_HOST}${_cdn_path}?${_cdn_query}"
|
||||
response="$(_post "$_payload" "$_url" "" "POST" "$_content_type")"
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Failed to call Baidu Cloud CDN API"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 response "$response"
|
||||
|
||||
if _contains "$response" "\"certId\""; then
|
||||
_info "Certificate deployed to Baidu Cloud CDN for: $DEPLOY_BAIDU_CDN_DOMAIN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to deploy certificate to Baidu Cloud CDN: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
# BCE v1 signing with canonical query string support.
|
||||
# The CDN endpoint uses ?action=put so it must be included in the canonical request.
|
||||
_baidu_cdn_bce_auth() {
|
||||
_method="$1"
|
||||
_uri="$2"
|
||||
_query="$3"
|
||||
_host="$4"
|
||||
_ts="$5"
|
||||
_expire="$6"
|
||||
_ct="$7"
|
||||
_payload_hash="$8"
|
||||
|
||||
_BAIDU_CDN_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_cdn_bce_encode_path "$_uri")"
|
||||
|
||||
_host_e="$(printf "%s" "$_host" | _url_encode upper-hex)"
|
||||
_date_e="$(printf "%s" "$_ts" | _url_encode upper-hex)"
|
||||
_ct_e="$(printf "%s" "$_ct" | _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}
|
||||
${_query}
|
||||
${_canonical_headers}"
|
||||
|
||||
_sk_hex="$(printf "%s" "$Baidu_SK" | _hex_dump | tr -d " ")"
|
||||
_signing_key="$(_baidu_cdn_hmac_sha256_hexkey "$_sk_hex" "$_auth_prefix")"
|
||||
_signing_key_hex="$(printf "%s" "$_signing_key" | _hex_dump | tr -d " ")"
|
||||
_signature="$(_baidu_cdn_hmac_sha256_hexkey "$_signing_key_hex" "$_canonical_request")"
|
||||
|
||||
_BAIDU_CDN_BCE_AUTH_RESULT="${_auth_prefix}/${_signed_headers}/${_signature}"
|
||||
}
|
||||
|
||||
_baidu_cdn_load_credentials() {
|
||||
Baidu_AK="${Baidu_AK:-$(_readaccountconf_mutable Baidu_AK)}"
|
||||
Baidu_SK="${Baidu_SK:-$(_readaccountconf_mutable Baidu_SK)}"
|
||||
|
||||
Baidu_AK="$(_baidu_cdn_trim_ws "$Baidu_AK")"
|
||||
Baidu_SK="$(_baidu_cdn_trim_ws "$Baidu_SK")"
|
||||
|
||||
if [ -z "$Baidu_AK" ] || [ -z "$Baidu_SK" ]; then
|
||||
_err "Baidu_AK and Baidu_SK are required"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable Baidu_AK "$Baidu_AK"
|
||||
_saveaccountconf_mutable Baidu_SK "$Baidu_SK"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_baidu_cdn_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"
|
||||
}
|
||||
|
||||
_baidu_cdn_trim_ws() {
|
||||
printf "%s" "$1" | tr '\r\n\t' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//'
|
||||
}
|
||||
|
||||
_baidu_cdn_json_escape() {
|
||||
_s="$1"
|
||||
_s="$(printf "%s" "$_s" | tr -d '\r\n')"
|
||||
printf "%s" "$_s" |
|
||||
sed 's/\\/\\\\/g; s/ /\\t/g' |
|
||||
_baidu_cdn_json_encode
|
||||
}
|
||||
|
||||
_baidu_cdn_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_cdn_hmac_sha256_hexkey() {
|
||||
_key_hex="$1"
|
||||
_msg="$2"
|
||||
printf "%s" "$_msg" | _hmac sha256 "$_key_hex" hex
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+11
-2
@@ -52,7 +52,15 @@ cpanel_uapi_deploy() {
|
||||
|
||||
# read cert and key files and urlencode both
|
||||
_cert=$(_url_encode <"$_ccert")
|
||||
_key=$(_url_encode <"$_ckey")
|
||||
# with --signcsr the private key was never handed to acme.sh, so the key
|
||||
# file does not exist; skip it instead of spilling a shell redirection
|
||||
# error on every renewal (cPanel keeps using the already-installed key)
|
||||
if [ -f "$_ckey" ]; then
|
||||
_key=$(_url_encode <"$_ckey")
|
||||
else
|
||||
_debug "Key file $_ckey does not exist (csr mode), not sending a key."
|
||||
_key=""
|
||||
fi
|
||||
|
||||
_debug2 _cert "$_cert"
|
||||
_debug2 _key "$_key"
|
||||
@@ -194,7 +202,8 @@ __cpanel_parse_response() {
|
||||
printf("%s%s=%s\n", prefix, $2, $3);
|
||||
}
|
||||
}' |
|
||||
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p'
|
||||
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p' |
|
||||
sed -e 's/^"//' -e 's/"$//' # YAML double-quotes values starting with '*' (wildcard subdomains)
|
||||
}
|
||||
|
||||
# Load parameter by prefix+name - fallback to default if not set, and save to config
|
||||
|
||||
+43
-3
@@ -3,6 +3,8 @@
|
||||
#DEPLOY_DOCKER_CONTAINER_LABEL="xxxxxxx"
|
||||
|
||||
#DEPLOY_DOCKER_CONTAINER_KEY_FILE="/path/to/key.pem"
|
||||
#DEPLOY_DOCKER_CONTAINER_KEY_MODE="0640"
|
||||
#DEPLOY_DOCKER_CONTAINER_KEY_OWNER="1000:1000"
|
||||
#DEPLOY_DOCKER_CONTAINER_CERT_FILE="/path/to/cert.pem"
|
||||
#DEPLOY_DOCKER_CONTAINER_CA_FILE="/path/to/ca.pem"
|
||||
#DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/path/to/fullchain.pem"
|
||||
@@ -71,6 +73,18 @@ docker_deploy() {
|
||||
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_FILE "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
|
||||
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
|
||||
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_CERT_FILE
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_CERT_FILE "$DEPLOY_DOCKER_CONTAINER_CERT_FILE"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
|
||||
@@ -112,6 +126,20 @@ docker_deploy() {
|
||||
if ! _docker_cp "$_cid" "$_ckey" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
|
||||
_info "Setting key file owner to $DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
|
||||
if ! _docker_exec "$_cid" chown "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
|
||||
_err "Can not change owner of key file in container"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
|
||||
_info "Setting key file mode to $DEPLOY_DOCKER_CONTAINER_KEY_MODE"
|
||||
if ! _docker_exec "$_cid" chmod "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
|
||||
_err "Can not change mode of key file in container"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
|
||||
@@ -189,10 +217,22 @@ _docker_exec() {
|
||||
_debug2 cjson "$cjson"
|
||||
execid="$(echo "$cjson" | cut -d '"' -f 4)"
|
||||
_debug execid "$execid"
|
||||
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": false,\"Tty\": false}")"
|
||||
#Detach:true is required for podman's docker-compatible API: with
|
||||
#Detach:false it streams the command output on the connection, so the
|
||||
#non-empty response was misread as an error (issue #4977). The real
|
||||
#result is checked via the exec inspect ExitCode below instead.
|
||||
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": true,\"Tty\": false}")"
|
||||
_debug2 ejson "$ejson"
|
||||
if [ "$ejson" ]; then
|
||||
_err "$ejson"
|
||||
_et=0
|
||||
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
|
||||
while _contains "$ijson" "\"Running\":true" && [ "$_et" -lt 10 ]; do
|
||||
sleep 1
|
||||
_et="$(_math "$_et" + 1)"
|
||||
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
|
||||
done
|
||||
_debug2 ijson "$ijson"
|
||||
if ! echo "$ijson" | _egrep_o "\"ExitCode\": *0[,}]" >/dev/null 2>&1; then
|
||||
_err "docker exec error: $ijson"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env sh
|
||||
# Script to deploy a certificate to FortiGate via API and set it as the current web GUI certificate.
|
||||
#
|
||||
# FortiGate's native ACME integration does not support wildcard certificates or domain validation,
|
||||
# and is not supported if you have a custom management web port (eg. DNAT web traffic).
|
||||
#
|
||||
# REQUIRED:
|
||||
# export FGT_HOST="fortigate_hostname-or-ip"
|
||||
# export FGT_TOKEN="fortigate_api_token"
|
||||
#
|
||||
# OPTIONAL:
|
||||
# export FGT_PORT="10443" # Custom HTTPS port (defaults to 443 if not set)
|
||||
#
|
||||
# Run `acme.sh --deploy -d example.com --deploy-hook fortigate --insecure` to use this script.
|
||||
# `--insecure` is required on first run if not already using a valid SSL certificate on firewall.
|
||||
|
||||
# Function to parse a FortiGate API response
|
||||
_fortigate_parse_response() {
|
||||
_fortigate_response="$1"
|
||||
_fortigate_func="$2"
|
||||
_fortigate_status=$(echo "$_fortigate_response" | _egrep_o '"status":[ ]*"[^"]*"' | cut -d '"' -f 4)
|
||||
|
||||
if [ "$_fortigate_status" != "success" ]; then
|
||||
_err "[$_fortigate_func] Operation failed. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "[$_fortigate_func] Operation successful."
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to deploy a base64-encoded certificate to the firewall
|
||||
_fortigate_deployer() {
|
||||
_fortigate_cert_base64=$(_base64 <"$_fortigate_cfullchain" | tr -d '\n')
|
||||
_fortigate_key_base64=$(_base64 <"$_fortigate_ckey" | tr -d '\n')
|
||||
_fortigate_payload=$(
|
||||
cat <<EOF
|
||||
{
|
||||
"type": "regular",
|
||||
"scope": "global",
|
||||
"certname": "$_fortigate_cert_name",
|
||||
"key_file_content": "$_fortigate_key_base64",
|
||||
"file_content": "$_fortigate_cert_base64"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/local/import"
|
||||
_debug "Uploading certificate via URL: $_fortigate_url"
|
||||
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "POST" "application/json")
|
||||
_debug "FortiGate API Response: $_fortigate_response"
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Deploying certificate" || return 1
|
||||
}
|
||||
|
||||
# Function to upload a CA certificate to the firewall
|
||||
# FortiGate does not automatically extract the CA from the full chain.
|
||||
_fortigate_upload_ca_cert() {
|
||||
_fortigate_ca_base64=$(_base64 <"$_fortigate_cca" | tr -d '\n')
|
||||
_fortigate_payload=$(
|
||||
cat <<EOF
|
||||
{
|
||||
"import_method": "file",
|
||||
"scope": "global",
|
||||
"file_content": "$_fortigate_ca_base64"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/ca/import"
|
||||
_debug "Uploading CA certificate via URL: $_fortigate_url"
|
||||
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "POST" "application/json")
|
||||
_debug "FortiGate API CA Response: $_fortigate_response"
|
||||
|
||||
# FortiGate error -328 means that the CA certificate already exists.
|
||||
if echo "$_fortigate_response" | grep -q '"error":[ ]*-328'; then
|
||||
_debug "CA certificate already exists. Skipping CA upload."
|
||||
return 0
|
||||
fi
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Deploying CA certificate" || return 1
|
||||
}
|
||||
|
||||
# Function to activate the new certificate
|
||||
_fortigate_set_active_web_cert() {
|
||||
_fortigate_payload=$(
|
||||
cat <<EOF
|
||||
{
|
||||
"admin-server-cert": "$_fortigate_cert_name"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/system/global"
|
||||
_debug "Setting GUI certificate..."
|
||||
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "PUT" "application/json")
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Assigning active certificate" || return 1
|
||||
}
|
||||
|
||||
# Function to clean up the previously deployed certificate
|
||||
_fortigate_cleanup_previous_certificate() {
|
||||
_getdeployconf FGT_LAST_CERT
|
||||
|
||||
if [ -n "$FGT_LAST_CERT" ] && [ "$FGT_LAST_CERT" != "$_fortigate_cert_name" ]; then
|
||||
_debug "Found previously deployed certificate: $FGT_LAST_CERT. Deleting it."
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/vpn.certificate/local/${FGT_LAST_CERT}"
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "" "$_fortigate_url" "" "DELETE" "application/json")
|
||||
_debug "Delete certificate API response: $_fortigate_response"
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Delete previous certificate" || return 1
|
||||
else
|
||||
_debug "No previous certificate found."
|
||||
fi
|
||||
}
|
||||
|
||||
# Main deploy-hook function
|
||||
fortigate_deploy() {
|
||||
# Include date and time to ensure unique names.
|
||||
_fortigate_cert_name="$(echo "$1" | sed 's/*/WILDCARD_/g')_$(date -u +"%Y-%m-%d_%H-%M-%S")"
|
||||
_fortigate_ckey="$2"
|
||||
_fortigate_cca="$4"
|
||||
_fortigate_cfullchain="$5"
|
||||
|
||||
if [ ! -f "$_fortigate_ckey" ] || [ ! -f "$_fortigate_cfullchain" ]; then
|
||||
_err "Valid key and/or certificate not found."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Save required environment variables if set; otherwise load saved values.
|
||||
for _fortigate_var in FGT_HOST FGT_TOKEN FGT_PORT; do
|
||||
if [ -n "$(eval echo "\$$_fortigate_var")" ]; then
|
||||
_debug "Detected ENV variable $_fortigate_var. Saving to file."
|
||||
_savedeployconf "$_fortigate_var" "$(eval echo "\$$_fortigate_var")" 1
|
||||
else
|
||||
_debug "Attempting to load variable $_fortigate_var from file."
|
||||
_getdeployconf "$_fortigate_var"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$FGT_HOST" ] || [ -z "$FGT_TOKEN" ]; then
|
||||
_err "FGT_HOST and FGT_TOKEN must be set."
|
||||
return 1
|
||||
fi
|
||||
|
||||
FGT_PORT="${FGT_PORT:-443}"
|
||||
_debug "Using FortiGate port: $FGT_PORT"
|
||||
|
||||
# Upload the new certificate.
|
||||
_fortigate_deployer || return 1
|
||||
|
||||
# Upload the CA certificate.
|
||||
if [ -n "$_fortigate_cca" ] && [ -f "$_fortigate_cca" ]; then
|
||||
_fortigate_upload_ca_cert || return 1
|
||||
else
|
||||
_debug "No CA certificate provided."
|
||||
fi
|
||||
|
||||
# Activate the new certificate.
|
||||
_fortigate_set_active_web_cert || return 1
|
||||
|
||||
# Delete the previously deployed certificate only after successful activation.
|
||||
_fortigate_cleanup_previous_certificate || return 1
|
||||
|
||||
# Save the new certificate name for cleanup during the next deployment.
|
||||
_savedeployconf "FGT_LAST_CERT" "$_fortigate_cert_name" 1
|
||||
}
|
||||
+1
-1
@@ -57,7 +57,7 @@ gcore_cdn_deploy() {
|
||||
_request="{\"username\":\"$Le_Deploy_gcore_cdn_username\",\"password\":\"$Le_Deploy_gcore_cdn_password\"}"
|
||||
_debug _request "$_request"
|
||||
export _H1="Content-Type:application/json"
|
||||
_response=$(_post "$_request" "https://api.gcore.com/auth/jwt/login")
|
||||
_response=$(_post "$_request" "https://api.gcore.com/iam/auth/jwt/login")
|
||||
_debug _response "$_response"
|
||||
_regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$"
|
||||
_debug _regex "$_regex"
|
||||
|
||||
+63
-52
@@ -43,7 +43,8 @@
|
||||
# needing to reload HAProxy. Default is "no".
|
||||
#
|
||||
# Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat
|
||||
# address format.
|
||||
# address format. The certificate can be deployed to a comma separated ',' list
|
||||
# of hosts ("TCP4:10.0.0.1:1999,TCP4:10.0.0.2:1999")
|
||||
#
|
||||
# export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock"
|
||||
#
|
||||
@@ -193,7 +194,6 @@ haproxy_deploy() {
|
||||
_issuer="${_pem}.issuer"
|
||||
_ocsp="${_pem}.ocsp"
|
||||
_reload="${Le_Deploy_haproxy_reload}"
|
||||
_statssock="${Le_Deploy_haproxy_stats_socket}"
|
||||
|
||||
_info "Deploying PEM file"
|
||||
# Create a temporary PEM file
|
||||
@@ -272,12 +272,18 @@ haproxy_deploy() {
|
||||
_cafile_argument=""
|
||||
fi
|
||||
_debug _cafile_argument "${_cafile_argument}"
|
||||
# if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed
|
||||
# OpenSSL 1.1+ expects -header Host=value (one argument), while
|
||||
# LibreSSL keeps the old two-argument form -header Host value at any
|
||||
# version (3.x/4.x), so it must be detected by name, not by number.
|
||||
_openssl_name=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f1)
|
||||
_openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
|
||||
_debug _openssl_name "${_openssl_name}"
|
||||
_debug _openssl_version "${_openssl_version}"
|
||||
_openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1)
|
||||
_openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2)
|
||||
if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
|
||||
if [ "${_openssl_name}" = "LibreSSL" ]; then
|
||||
_header_sep=" "
|
||||
elif [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
|
||||
_header_sep="="
|
||||
else
|
||||
_header_sep=" "
|
||||
@@ -327,62 +333,67 @@ haproxy_deploy() {
|
||||
|
||||
# Update certificate over HAProxy stats socket or master CLI.
|
||||
if _exists socat; then
|
||||
# look for the certificate on the stats socket, to chose between updating or creating one
|
||||
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
|
||||
_debug _socat_cert_cmd "${_socat_cert_cmd}"
|
||||
eval "${_socat_cert_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_newcert="1"
|
||||
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
|
||||
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
|
||||
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
|
||||
eval "${_socat_crtlist_show_cmd}"
|
||||
IFS=','
|
||||
for _statssock in ${Le_Deploy_haproxy_stats_socket}; do
|
||||
# look for the certificate on the stats socket, to choose between updating or creating one
|
||||
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
|
||||
_debug _socat_cert_cmd "${_socat_cert_cmd}"
|
||||
eval "${_socat_cert_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
|
||||
return "${_ret}"
|
||||
_newcert="1"
|
||||
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
|
||||
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
|
||||
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
|
||||
eval "${_socat_crtlist_show_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
|
||||
return "${_ret}"
|
||||
fi
|
||||
# create a new certificate
|
||||
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
|
||||
_debug _socat_new_cmd "${_socat_new_cmd}"
|
||||
eval "${_socat_new_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't create '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
else
|
||||
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
fi
|
||||
# create a new certificate
|
||||
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
|
||||
_debug _socat_new_cmd "${_socat_new_cmd}"
|
||||
eval "${_socat_new_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't create '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
else
|
||||
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
fi
|
||||
_socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'"
|
||||
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
|
||||
eval "${_socat_cert_set_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't update '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
|
||||
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
|
||||
eval "${_socat_cert_commit_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't commit '${_pem}' in haproxy"
|
||||
return ${_ret}
|
||||
fi
|
||||
if [ "${_newcert}" = "1" ]; then
|
||||
# if this is a new certificate, it needs to be inserted into the crt-list`
|
||||
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
|
||||
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
|
||||
eval "${_socat_cert_add_cmd}"
|
||||
# printf %b, not "echo -e": dash's echo has no -e and sends a literal "-e " to the socket.
|
||||
# "Transaction updated" is replied instead of "created" when an uncommitted transaction exists.
|
||||
_socat_cert_set_cmd="printf '%b\n' '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -qE 'Transaction (created|updated)'"
|
||||
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
|
||||
eval "${_socat_cert_set_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't update '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
fi
|
||||
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
|
||||
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
|
||||
eval "${_socat_cert_commit_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't commit '${_pem}' in haproxy"
|
||||
return ${_ret}
|
||||
fi
|
||||
if [ "${_newcert}" = "1" ]; then
|
||||
# if this is a new certificate, it needs to be inserted into the crt-list`
|
||||
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
|
||||
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
|
||||
eval "${_socat_cert_add_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't update '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
_err "'socat' is not available, couldn't update over ${_socketname}"
|
||||
fi
|
||||
|
||||
+2
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
_cleardeployconf "$_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"
|
||||
}
|
||||
+3
-1
@@ -54,6 +54,8 @@ mydevil_deploy() {
|
||||
# Usage: ip=$(mydevil_get_ip domain.com)
|
||||
# echo $ip
|
||||
mydevil_get_ip() {
|
||||
devil dns list "$1" | cut -w -s -f 3,7 | grep "^A$(printf '\t')" | cut -w -s -f 2 || return 1
|
||||
# tr squeezes runs of blanks into one tab so plain cut works everywhere;
|
||||
# cut -w is BSD-only and unknown to GNU coreutils
|
||||
devil dns list "$1" | tr -s ' \t' '\t' | cut -s -f 3,7 | grep "^A$(printf '\t')" | cut -s -f 2 || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
+30
-19
@@ -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
|
||||
@@ -296,9 +296,20 @@ panos_deploy() {
|
||||
_err "Unable to generate an API key. The user and pass may be invalid or not authorized to generate a new key. Please check the PANOS_USER and PANOS_PASS credentials and try again"
|
||||
return 1
|
||||
else
|
||||
deployer cert
|
||||
deployer key
|
||||
deployer commit
|
||||
# A commit of a failed import would leave a mismatched cert/key pair
|
||||
# on the firewall and can lock the admin out of the management
|
||||
# interface, see https://github.com/acmesh-official/acme.sh/issues/4716
|
||||
if ! deployer cert; then
|
||||
_err "Cert import failed. Aborting without committing."
|
||||
return 1
|
||||
fi
|
||||
if ! deployer key; then
|
||||
_err "Key import failed. Aborting without committing. Warning: the firewall now has an uncommitted mismatched cert/key pair in its candidate config."
|
||||
return 1
|
||||
fi
|
||||
if ! deployer commit; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$_panos_template_stack" ]; then
|
||||
# try to get job status for 20 times in 30 sec interval
|
||||
i=0
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+9
-1
@@ -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")"
|
||||
|
||||
+2
-1
@@ -125,7 +125,7 @@ routeros_deploy() {
|
||||
_savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT"
|
||||
_savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD"
|
||||
_savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD"
|
||||
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES"
|
||||
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES" "base64"
|
||||
|
||||
# push key to routeros
|
||||
if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then
|
||||
@@ -143,6 +143,7 @@ comment=\"generated by routeros deploy script in acme.sh\" \
|
||||
source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\
|
||||
\n/certificate remove [ find name=$_cdomain.cer_1 ];\
|
||||
\n/certificate remove [ find name=$_cdomain.cer_2 ];\
|
||||
\n/certificate remove [ find name=$_cdomain.cer_3 ];\
|
||||
\ndelay 1;\
|
||||
\n/certificate import file-name=\\\"$_cdomain.cer\\\" passphrase=\\\"\\\";\
|
||||
\n/certificate import file-name=\\\"$_cdomain.key\\\" passphrase=\\\"\\\";\
|
||||
|
||||
+36
-5
@@ -25,7 +25,8 @@
|
||||
# export DEPLOY_SSH_MULTI_CALL="" # yes or no, default to no or previously saved value
|
||||
# export DEPLOY_SSH_USE_SCP="" yes or no, default to no
|
||||
# export DEPLOY_SSH_SCP_CMD="" defaults to "scp -q"
|
||||
#
|
||||
# export DEPLOY_SSH_REMOTE_SHELL="" # defaults to sh -c
|
||||
# export DEPLOY_SSH_REMOTE_CMD_QUOTE="" # yes or no, defaults to yes
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
@@ -71,6 +72,24 @@ ssh_deploy() {
|
||||
fi
|
||||
_savedeployconf DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD"
|
||||
|
||||
# REMOTE_SHELL is optional. If not provided then use sh
|
||||
_migratedeployconf Le_Deploy_ssh_remote_shell DEPLOY_SSH_REMOTE_SHELL
|
||||
_getdeployconf DEPLOY_SSH_REMOTE_SHELL
|
||||
_debug2 DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL"
|
||||
if [ -z "$DEPLOY_SSH_REMOTE_SHELL" ]; then
|
||||
DEPLOY_SSH_REMOTE_SHELL="sh -c"
|
||||
fi
|
||||
_savedeployconf DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL"
|
||||
|
||||
# REMOTE_CMD_QUOTE is optional. If not provided then yes
|
||||
_migratedeployconf Le_Deploy_ssh_remote_cmd_quote DEPLOY_SSH_REMOTE_CMD_QUOTE
|
||||
_getdeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE
|
||||
_debug2 DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE"
|
||||
if [ -z "$DEPLOY_SSH_REMOTE_CMD_QUOTE" ]; then
|
||||
DEPLOY_SSH_REMOTE_CMD_QUOTE="yes"
|
||||
fi
|
||||
_savedeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE"
|
||||
|
||||
# BACKUP is optional. If not provided then default to previously saved value or yes.
|
||||
_migratedeployconf Le_Deploy_ssh_backup DEPLOY_SSH_BACKUP
|
||||
_getdeployconf DEPLOY_SSH_BACKUP
|
||||
@@ -170,10 +189,16 @@ ssh_deploy() {
|
||||
_info "Required commands batched and sent in single call to remote host"
|
||||
fi
|
||||
|
||||
_returnCode=0
|
||||
_deploy_ssh_servers="$DEPLOY_SSH_SERVER"
|
||||
for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do
|
||||
_ssh_deploy
|
||||
if ! _ssh_deploy; then
|
||||
# in case of an error, remember it, but keep going for the remaining servers
|
||||
_returnCode=1
|
||||
fi
|
||||
done
|
||||
|
||||
return $_returnCode
|
||||
}
|
||||
|
||||
_ssh_deploy() {
|
||||
@@ -238,6 +263,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"
|
||||
@@ -426,9 +453,13 @@ _ssh_remote_cmd() {
|
||||
_secure_debug "Remote commands to execute: $_cmd"
|
||||
_info "Submitting sequence of commands to remote server by $_ssh_cmd"
|
||||
|
||||
# quotations in bash cmd below intended. Squash travis spellcheck error
|
||||
# shellcheck disable=SC2029
|
||||
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" sh -c "'$_cmd'"
|
||||
if [ "$DEPLOY_SSH_REMOTE_CMD_QUOTE" = "yes" ]; then
|
||||
# quotations in bash cmd below intended. Squash travis spellcheck error
|
||||
# shellcheck disable=SC2029
|
||||
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "'$_cmd'"
|
||||
else
|
||||
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "$_cmd"
|
||||
fi
|
||||
_err_code="$?"
|
||||
|
||||
if [ "$_err_code" != "0" ]; then
|
||||
|
||||
@@ -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}"
|
||||
|
||||
+17
-22
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env sh
|
||||
|
||||
################################################################################
|
||||
# ACME.sh 3rd party deploy plugin for Synology DSM
|
||||
@@ -72,7 +72,7 @@ synology_dsm_deploy() {
|
||||
|
||||
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
|
||||
if ! _exists synouser || ! _exists synogroup || ! _exists synosetkeyvalue; then
|
||||
_err "Missing required tools to creat temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
|
||||
_err "Missing required tools to create temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
|
||||
_err "Notice: temp admin user authorization method only supports local deployment on DSM."
|
||||
return 1
|
||||
fi
|
||||
@@ -234,11 +234,11 @@ synology_dsm_deploy() {
|
||||
fi
|
||||
fi
|
||||
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
|
||||
_debug2 error_code "$error_code"
|
||||
# Account has 2FA-OTP enabled, since error 403 reported.
|
||||
# https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf
|
||||
if [ "$error_code" == "403" ]; then
|
||||
if [ "$error_code" = "403" ]; then
|
||||
if [ -z "$SYNO_DEVICE_NAME" ]; then
|
||||
printf "Enter device name or leave empty for default (CertRenewal): "
|
||||
read -r SYNO_DEVICE_NAME
|
||||
@@ -269,27 +269,27 @@ synology_dsm_deploy() {
|
||||
_secure_debug2 SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
|
||||
fi
|
||||
fi
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
|
||||
_debug2 error_code "$error_code"
|
||||
fi
|
||||
|
||||
if [ -n "$error_code" ]; then
|
||||
if [ "$error_code" == "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
|
||||
if [ "$error_code" = "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
|
||||
_cleardeployconf SYNO_DEVICE_ID
|
||||
_err "Failed to authenticate with SYNO_DEVICE_ID (may expired or invalid), please try again in a new terminal window."
|
||||
elif [ "$error_code" == "404" ]; then
|
||||
_err "Failed to authenticate with SYNO_DEVICE_ID (may be expired or invalid), please try again in a new terminal window."
|
||||
elif [ "$error_code" = "404" ]; then
|
||||
_err "Failed to authenticate with provided 2FA-OTP code, please try again in a new terminal window."
|
||||
elif [ "$error_code" == "406" ]; then
|
||||
elif [ "$error_code" = "406" ]; then
|
||||
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
|
||||
_err "Failed with unexcepted error, please report this by providing full log with '--debug 3'."
|
||||
else
|
||||
_err "Enforce auth with 2FA-OTP enabled, please configure the user to enable 2FA-OTP to continue."
|
||||
fi
|
||||
elif [ "$error_code" == "400" ]; then
|
||||
elif [ "$error_code" = "400" ]; then
|
||||
_err "Failed to authenticate, no such account or incorrect password."
|
||||
elif [ "$error_code" == "401" ]; then
|
||||
elif [ "$error_code" = "401" ]; then
|
||||
_err "Failed to authenticate with a non-existent account."
|
||||
elif [ "$error_code" == "408" ] || [ "$error_code" == "409" ] || [ "$error_code" == "410" ]; then
|
||||
elif [ "$error_code" = "408" ] || [ "$error_code" = "409" ] || [ "$error_code" = "410" ]; then
|
||||
_err "Failed to authenticate, the account password has expired or must be changed."
|
||||
else
|
||||
_err "Failed to authenticate with error: $error_code."
|
||||
@@ -322,8 +322,8 @@ synology_dsm_deploy() {
|
||||
_savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
|
||||
_savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME"
|
||||
else
|
||||
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME"
|
||||
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD"
|
||||
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME" "base64"
|
||||
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" "base64"
|
||||
_savedeployconf SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
|
||||
_savedeployconf SYNO_DEVICE_NAME "$SYNO_DEVICE_NAME"
|
||||
fi
|
||||
@@ -336,7 +336,7 @@ synology_dsm_deploy() {
|
||||
id=$(echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\"id\":\"\([^\"]*\).*/\1/p")
|
||||
_debug2 id "$id"
|
||||
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
|
||||
_debug2 error_code "$error_code"
|
||||
if [ -n "$error_code" ]; then
|
||||
if [ "$error_code" -eq 105 ]; then
|
||||
@@ -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
|
||||
@@ -424,11 +424,6 @@ _temp_admin_cleanup() {
|
||||
fi
|
||||
}
|
||||
|
||||
#_cleardeployconf key
|
||||
_cleardeployconf() {
|
||||
_cleardomainconf "SAVED_$1"
|
||||
}
|
||||
|
||||
# key
|
||||
_check2cleardeployconfexp() {
|
||||
_key="$1"
|
||||
|
||||
+25
-5
@@ -16,7 +16,12 @@
|
||||
#
|
||||
# # API KEY
|
||||
# # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
|
||||
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
|
||||
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI>"
|
||||
# Optional:
|
||||
# export DEPLOY_TRUENAS_HOSTNAME="<TRUENAS_HOSTNAME_OR_IP>"
|
||||
# export DEPLOY_TRUENAS_PROTOCOL="wss" # ws or wss
|
||||
# export DEPLOY_TRUENAS_PORT="443" # optional, e.g. 80, 443, 8443
|
||||
|
||||
#
|
||||
|
||||
### Private functions
|
||||
@@ -56,7 +61,6 @@ _ws_call() {
|
||||
_ws_upload_cert() {
|
||||
|
||||
/usr/bin/env python - <<EOF
|
||||
|
||||
import sys
|
||||
|
||||
from truenas_api_client import Client
|
||||
@@ -78,7 +82,6 @@ with Client(uri="$_ws_uri") as c:
|
||||
print("R:0")
|
||||
print("E:_ws_upload_cert error!")
|
||||
sys.exit(7)
|
||||
|
||||
EOF
|
||||
|
||||
return $?
|
||||
@@ -181,6 +184,8 @@ truenas_ws_deploy() {
|
||||
_getdeployconf DEPLOY_TRUENAS_APIKEY
|
||||
_getdeployconf DEPLOY_TRUENAS_HOSTNAME
|
||||
_getdeployconf DEPLOY_TRUENAS_PROTOCOL
|
||||
_getdeployconf DEPLOY_TRUENAS_PORT
|
||||
|
||||
# Check API Key
|
||||
if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
|
||||
_err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
|
||||
@@ -196,7 +201,21 @@ truenas_ws_deploy() {
|
||||
_info "TrueNAS protocol not set. Using 'ws'."
|
||||
DEPLOY_TRUENAS_PROTOCOL="ws"
|
||||
fi
|
||||
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
|
||||
|
||||
# Check port, optional
|
||||
if [ -n "$DEPLOY_TRUENAS_PORT" ]; then
|
||||
case "$DEPLOY_TRUENAS_PORT" in
|
||||
'' | *[!0-9]*)
|
||||
_err "Invalid TrueNAS port '$DEPLOY_TRUENAS_PORT'. DEPLOY_TRUENAS_PORT must be numeric."
|
||||
return 8
|
||||
;;
|
||||
esac
|
||||
|
||||
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME:$DEPLOY_TRUENAS_PORT/websocket"
|
||||
else
|
||||
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
|
||||
fi
|
||||
|
||||
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
|
||||
_debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
|
||||
_debug _ws_uri "$_ws_uri"
|
||||
@@ -216,13 +235,14 @@ truenas_ws_deploy() {
|
||||
|
||||
if [ "$_ws_response" != "TRUE" ]; then
|
||||
_err "TrueNAS is not ready."
|
||||
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL."
|
||||
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME, DEPLOY_TRUENAS_PROTOCOL and DEPLOY_TRUENAS_PORT."
|
||||
_err "Verify API key."
|
||||
return 2
|
||||
fi
|
||||
_savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
|
||||
_savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
|
||||
_savedeployconf DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
|
||||
_savedeployconf DEPLOY_TRUENAS_PORT "$DEPLOY_TRUENAS_PORT"
|
||||
_info "TrueNAS health: OK"
|
||||
|
||||
########## System info
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# install a certificate on a Windows host over OpenSSH and bind it to the Remote
|
||||
# Desktop listener (RDP-Tcp).
|
||||
#
|
||||
# One ssh invocation does the whole job:
|
||||
# * the PFX is built locally, base64'd, and embedded as a string literal
|
||||
# inside a generated PowerShell script;
|
||||
# * the script is piped to `powershell.exe -Command -` over ssh. No scp,
|
||||
# no temp files on the Windows host.
|
||||
#
|
||||
# First run:
|
||||
# export DEPLOY_WIN_RDP_HOST=winserver.example.com
|
||||
# acme.sh --deploy -d winserver.example.com --deploy-hook windows_rdp
|
||||
#
|
||||
# Available variables:
|
||||
# DEPLOY_WIN_RDP_HOST required SSH host
|
||||
# DEPLOY_WIN_RDP_USER optional SSH user, must be a local administrator (can also by set via ssh_config)
|
||||
# DEPLOY_WIN_RDP_PORT optional SSH port, default 22
|
||||
# DEPLOY_WIN_RDP_SSH_OPTS optional extra ssh options, e.g.
|
||||
# "-i /root/.ssh/win_id_ed25519 -o StrictHostKeyChecking=yes"
|
||||
# DEPLOY_WIN_RDP_LISTENER optional RDP listener name, default RDP-Tcp
|
||||
# DEPLOY_WIN_RDP_RESTART optional "1" to restart TermService after install.
|
||||
# Active RDP sessions will drop!
|
||||
|
||||
windows_rdp_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"
|
||||
|
||||
if ! _exists "ssh"; then
|
||||
_err "ssh is required but was not found in PATH."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- configuration ------------------------------------------------------
|
||||
_getdeployconf DEPLOY_WIN_RDP_HOST
|
||||
_getdeployconf DEPLOY_WIN_RDP_USER
|
||||
_getdeployconf DEPLOY_WIN_RDP_PORT
|
||||
_getdeployconf DEPLOY_WIN_RDP_SSH_OPTS
|
||||
_getdeployconf DEPLOY_WIN_RDP_LISTENER
|
||||
_getdeployconf DEPLOY_WIN_RDP_RESTART
|
||||
|
||||
if [ -z "$DEPLOY_WIN_RDP_HOST" ]; then
|
||||
_err "DEPLOY_WIN_RDP_HOST must be set."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_savedeployconf DEPLOY_WIN_RDP_HOST "$DEPLOY_WIN_RDP_HOST"
|
||||
[ -n "$DEPLOY_WIN_RDP_USER" ] && _savedeployconf DEPLOY_WIN_RDP_USER "$DEPLOY_WIN_RDP_USER"
|
||||
[ -n "$DEPLOY_WIN_RDP_PORT" ] && _savedeployconf DEPLOY_WIN_RDP_PORT "$DEPLOY_WIN_RDP_PORT"
|
||||
[ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ] && _savedeployconf DEPLOY_WIN_RDP_SSH_OPTS "$DEPLOY_WIN_RDP_SSH_OPTS"
|
||||
[ -n "$DEPLOY_WIN_RDP_LISTENER" ] && _savedeployconf DEPLOY_WIN_RDP_LISTENER "$DEPLOY_WIN_RDP_LISTENER"
|
||||
[ -n "$DEPLOY_WIN_RDP_RESTART" ] && _savedeployconf DEPLOY_WIN_RDP_RESTART "$DEPLOY_WIN_RDP_RESTART"
|
||||
|
||||
_port="${DEPLOY_WIN_RDP_PORT:-22}"
|
||||
_listener="${DEPLOY_WIN_RDP_LISTENER:-RDP-Tcp}"
|
||||
if [ -n "$DEPLOY_WIN_RDP_USER" ]; then
|
||||
_target="$DEPLOY_WIN_RDP_USER@$DEPLOY_WIN_RDP_HOST"
|
||||
else
|
||||
_target="$DEPLOY_WIN_RDP_HOST"
|
||||
fi
|
||||
_pfx_pass="acme"
|
||||
|
||||
# ---- build thumbprint + PFX locally ------------------------------------
|
||||
_thumb="$(_fingerprint "$_ccert" 'sha1')"
|
||||
if [ -z "$_thumb" ]; then
|
||||
_err "Failed to compute certificate thumbprint."
|
||||
return 1
|
||||
fi
|
||||
_debug "Thumbprint: $_thumb"
|
||||
|
||||
_debug "Building PFX at $_pfx_file"
|
||||
_pfx_file="$(_mktemp)"
|
||||
if ! _toPkcs "$_pfx_file" "$_ckey" "$_ccert" "$_cca" "$_pfx_pass"; then
|
||||
_err "Failed to build PFX archive."
|
||||
rm -f "$_pfx_file"
|
||||
return 1
|
||||
fi
|
||||
_pfx_b64=$(_base64 "multiline" <"$_pfx_file")
|
||||
rm -f "$_pfx_file"
|
||||
|
||||
# ---- build installer script --------------------------------------------
|
||||
if [ "$DEPLOY_WIN_RDP_RESTART" = "1" ]; then
|
||||
_restart_ps='Restart-Service -Name TermService -Force'
|
||||
else
|
||||
_restart_ps='# New RdP connections will pick up the new cert automatically.'
|
||||
fi
|
||||
|
||||
# Escape every literal `$` with `\$` so the shell does not expand it.
|
||||
# Values substituted from shell: $_pfx_b64, $_pfx_pass, $_thumb, $_listener.
|
||||
_ps1=$(
|
||||
cat <<PSEOF
|
||||
|
||||
\$ErrorActionPreference = 'Stop'
|
||||
|
||||
\$pfxBytes = [Convert]::FromBase64String('${_pfx_b64}')
|
||||
|
||||
# Note: It is quite important to use a X509Certificate2Collection here in any case, since we otherwise
|
||||
# could run into quite a lot of trouble when importing the certificate including its entire chain
|
||||
# and its private key. Windows might behave arbitrarily and not consistently import the certificate
|
||||
# at all - unless "Exportable" is included in the storage flags. However, then the certificate seems
|
||||
# unaccessible to TermService for some weird reasons despite all permissions being set (at least on my
|
||||
# Win 11 lab machine). This might be some security setting that prevents TermService from working with
|
||||
# exportable keys? I don't know - importing the entire collection including chain or not always fixes
|
||||
# the issues.
|
||||
#
|
||||
# Note2: If you should have kicked yourself out for some reason, then deleting the certificate will make
|
||||
# TermService restore the original, self-signed certificate after at least after the second login attempt.
|
||||
# Deleting the certificate can be easily accomplished via the Powershell, since SSH access will still be
|
||||
# present in any case - the following command should get you out of trouble:
|
||||
# \$cert = Get-ChildItem -Path 'Cert:\LocalMachine\My\\${_thumb}' | Select-Object -First 1 | Remove-Item
|
||||
|
||||
\$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]'MachineKeySet,PersistKeySet'
|
||||
\$certs = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
|
||||
\$certs.Import(\$pfxBytes, '${_pfx_pass}', \$flags)
|
||||
|
||||
\$store = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'LocalMachine')
|
||||
\$store.Open('ReadWrite')
|
||||
\$store.AddRange(\$certs)
|
||||
\$store.Close()
|
||||
Write-Host "Installed certs into LocalMachine\\My"
|
||||
|
||||
\$ts = Get-CimInstance -Namespace root/cimv2/terminalservices -ClassName Win32_TSGeneralSetting -Filter "TerminalName='${_listener}'"
|
||||
if (-not \$ts) { throw "Listener '${_listener}' not found." }
|
||||
Set-CimInstance -InputObject \$ts -Property @{SSLCertificateSHA1Hash="${_thumb}"}
|
||||
Write-Host "Listener ${_listener} now uses ${_thumb}"
|
||||
|
||||
${_restart_ps}
|
||||
PSEOF
|
||||
)
|
||||
_debug "Powershell script:${_ps1}"
|
||||
|
||||
# ---- run over a single ssh connection ----------------------------------
|
||||
_ssh_opts="-o BatchMode=yes -p $_port"
|
||||
if [ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ]; then
|
||||
_ssh_opts="$_ssh_opts $DEPLOY_WIN_RDP_SSH_OPTS"
|
||||
fi
|
||||
|
||||
_info "Deploying to $DEPLOY_WIN_RDP_HOST ..."
|
||||
# shellcheck disable=SC2086
|
||||
if ! printf '%s\n' "$_ps1" | ssh $_ssh_opts "$_target" \
|
||||
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -'; then
|
||||
_err "Remote install failed. Re-run acme.sh with --debug to see the PowerShell output."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Certificate for $_cdomain deployed and bound to $_listener on $DEPLOY_WIN_RDP_HOST."
|
||||
return 0
|
||||
}
|
||||
+41
-10
@@ -7,6 +7,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_1984hosting
|
||||
Options:
|
||||
One984HOSTING_Username Username
|
||||
One984HOSTING_Password Password
|
||||
One984HOSTING_TOTP_Secret Base32 TOTP shared secret. Required only if the account has 2FA enabled. Requires oathtool. Used to mint the OTP code automatically at login so cron renewals keep working.
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2851
|
||||
Author: Adrian Fedoreanu
|
||||
'
|
||||
@@ -124,11 +125,28 @@ _1984hosting_login() {
|
||||
_debug "Login to 1984Hosting as user $One984HOSTING_Username."
|
||||
username=$(printf '%s' "$One984HOSTING_Username" | _url_encode)
|
||||
password=$(printf '%s' "$One984HOSTING_Password" | _url_encode)
|
||||
url="https://1984.hosting/api/auth/"
|
||||
|
||||
_get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken"
|
||||
csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
|
||||
sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
|
||||
# When 2FA is enabled, mint a fresh TOTP code from the stored shared secret.
|
||||
# Empty otpkey is accepted by the server when 2FA is off.
|
||||
otpkey=""
|
||||
if [ -n "$One984HOSTING_TOTP_Secret" ]; then
|
||||
if ! _exists oathtool; then
|
||||
_err "oathtool is required to use One984HOSTING_TOTP_Secret for 2FA. Please install it."
|
||||
return 1
|
||||
fi
|
||||
otpcode="$(oathtool --base32 --totp "$One984HOSTING_TOTP_Secret" 2>/dev/null)"
|
||||
if [ -z "$otpcode" ]; then
|
||||
_err "Failed to generate TOTP code from One984HOSTING_TOTP_Secret."
|
||||
return 1
|
||||
fi
|
||||
otpkey="$(printf '%s' "$otpcode" | _url_encode)"
|
||||
fi
|
||||
|
||||
# Fetch the login page to obtain CSRF and session cookies.
|
||||
# Note: _get sets the global 'url', so assign the auth URL afterwards.
|
||||
_get "https://1984.hosting/accounts/login/" >/dev/null
|
||||
csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
|
||||
if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
|
||||
_err "One or more cookies are empty: '$csrftoken', '$sessionid'."
|
||||
@@ -140,17 +158,23 @@ _1984hosting_login() {
|
||||
csrf_header=$(echo "$csrftoken" | sed 's/csrftoken=//' | _head_n 1)
|
||||
export _H3="X-CSRFToken: $csrf_header"
|
||||
|
||||
response="$(_post "username=$username&password=$password&otpkey=" $url)"
|
||||
url="https://1984.hosting/api/auth/"
|
||||
response="$(_post "username=$username&password=$password&otpkey=$otpkey" "$url")"
|
||||
response="$(echo "$response" | _normalizeJson)"
|
||||
_debug2 response "$response"
|
||||
|
||||
if _contains "$response" '"loggedin": true'; then
|
||||
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
|
||||
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
|
||||
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
export One984HOSTING_SESSIONID_COOKIE
|
||||
export One984HOSTING_CSRFTOKEN_COOKIE
|
||||
_saveaccountconf_mutable One984HOSTING_Username "$One984HOSTING_Username"
|
||||
_saveaccountconf_mutable One984HOSTING_Password "$One984HOSTING_Password"
|
||||
if [ -n "$One984HOSTING_TOTP_Secret" ]; then
|
||||
_saveaccountconf_mutable One984HOSTING_TOTP_Secret "$One984HOSTING_TOTP_Secret"
|
||||
else
|
||||
_clearaccountconf_mutable One984HOSTING_TOTP_Secret
|
||||
fi
|
||||
_saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE"
|
||||
_saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE"
|
||||
return 0
|
||||
@@ -161,6 +185,7 @@ _1984hosting_login() {
|
||||
_check_credentials() {
|
||||
One984HOSTING_Username="${One984HOSTING_Username:-$(_readaccountconf_mutable One984HOSTING_Username)}"
|
||||
One984HOSTING_Password="${One984HOSTING_Password:-$(_readaccountconf_mutable One984HOSTING_Password)}"
|
||||
One984HOSTING_TOTP_Secret="${One984HOSTING_TOTP_Secret:-$(_readaccountconf_mutable One984HOSTING_TOTP_Secret)}"
|
||||
if [ -z "$One984HOSTING_Username" ] || [ -z "$One984HOSTING_Password" ]; then
|
||||
One984HOSTING_Username=""
|
||||
One984HOSTING_Password=""
|
||||
@@ -225,9 +250,15 @@ _get_root() {
|
||||
|
||||
# Usage: _get_zone_id url domain.com
|
||||
# Returns zone id for domain.com
|
||||
# Memoized per-domain so add/rm don't re-fetch the same zone list within a run.
|
||||
# Keyed on domain (not url) since the url is always the domains listing.
|
||||
_get_zone_id() {
|
||||
url=$1
|
||||
domain=$2
|
||||
if [ "$_zone_id_for" = "$domain" ] && [ -n "$_zone_id" ]; then
|
||||
_debug2 _zone_id "$_zone_id (cached)"
|
||||
return 0
|
||||
fi
|
||||
_htmlget "$url" "$domain"
|
||||
_zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+' | _head_n 1)"
|
||||
_debug2 _zone_id "$_zone_id"
|
||||
@@ -235,6 +266,7 @@ _get_zone_id() {
|
||||
_err "Error getting _zone_id for $2."
|
||||
return 1
|
||||
fi
|
||||
_zone_id_for="$domain"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -257,9 +289,8 @@ _htmlget() {
|
||||
|
||||
# Add extra headers to request
|
||||
_authpost() {
|
||||
url="https://1984.hosting/domains"
|
||||
_get_zone_id "$url" "$_domain"
|
||||
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")"
|
||||
_get_zone_id "https://1984.hosting/domains" "$_domain"
|
||||
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | sed 's/csrftoken=//' | _head_n 1)"
|
||||
export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE"
|
||||
export _H2="Referer: https://1984.hosting/domains/$_zone_id"
|
||||
export _H3="X-CSRFToken: $csrf_header"
|
||||
|
||||
+11
-1
@@ -37,6 +37,16 @@ dns_acmedns_add() {
|
||||
ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readdomainconf ACMEDNS_PASSWORD)}"
|
||||
ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readdomainconf ACMEDNS_SUBDOMAIN)}"
|
||||
|
||||
#for compatibility: old versions stored ACMEDNS_UPDATE_URL in the account
|
||||
#conf (issue 3899). Do not clear it here: it must stay available for the
|
||||
#other domains that have not migrated to their domain conf yet.
|
||||
if [ -z "$ACMEDNS_BASE_URL" ]; then
|
||||
_acmedns_update_url="$(_readaccountconf_mutable ACMEDNS_UPDATE_URL)"
|
||||
if [ "$_acmedns_update_url" ]; then
|
||||
ACMEDNS_BASE_URL="$(echo "$_acmedns_update_url" | sed 's#/update$##')"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$ACMEDNS_BASE_URL" = "" ]; then
|
||||
ACMEDNS_BASE_URL="https://auth.acme-dns.io"
|
||||
fi
|
||||
@@ -71,7 +81,7 @@ dns_acmedns_add() {
|
||||
data="{\"subdomain\":\"$ACMEDNS_SUBDOMAIN\", \"txt\": \"$txtvalue\"}"
|
||||
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST")"
|
||||
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST" "application/json")"
|
||||
_debug response "$response"
|
||||
|
||||
if ! echo "$response" | grep "\"$txtvalue\"" >/dev/null; then
|
||||
|
||||
+30
-13
@@ -18,7 +18,9 @@ Ali_DNS_API="https://alidns.aliyuncs.com/"
|
||||
|
||||
#Usage: dns_ali_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_ali_add() {
|
||||
fulldomain=$1
|
||||
# the API only accepts punycode for IDN domains, and a raw UTF-8 domain
|
||||
# also breaks the request signature (issue 4733)
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
_prepare_ali_credentials || return 1
|
||||
@@ -33,7 +35,7 @@ dns_ali_add() {
|
||||
}
|
||||
|
||||
dns_ali_rm() {
|
||||
fulldomain=$1
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
|
||||
Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
|
||||
@@ -69,8 +71,8 @@ _ali_rest() {
|
||||
ign="$2"
|
||||
mtd="${3:-GET}"
|
||||
|
||||
signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _url_encode upper-hex)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
|
||||
signature=$(printf "%s" "$signature" | _url_encode upper-hex)
|
||||
signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _ali_urlencode_upper)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
|
||||
signature=$(printf "%s" "$signature" | _ali_urlencode_upper)
|
||||
url="$endpoint?Signature=$signature"
|
||||
|
||||
if [ "$mtd" = "GET" ]; then
|
||||
@@ -96,13 +98,28 @@ _ali_rest() {
|
||||
fi
|
||||
}
|
||||
|
||||
_ali_nonce() {
|
||||
#_head_n 1 </dev/urandom | _digest "sha256" hex | cut -c 1-31
|
||||
#Not so good...
|
||||
date +"%s%N" | sed 's/%N//g'
|
||||
# stdin stdout
|
||||
# The Aliyun signature requires percent-encoding with upper-case hex.
|
||||
# Do not use "_url_encode upper-hex" here: this file is also bundled by
|
||||
# third parties (e.g. Proxmox VE proxmox-acme) whose older copies of the
|
||||
# acme.sh function library ignore the upper-hex argument and output
|
||||
# lower-case hex, which invalidates the signature.
|
||||
# https://github.com/acmesh-official/acme.sh/issues/6272
|
||||
_ali_urlencode_upper() {
|
||||
{
|
||||
_url_encode
|
||||
echo
|
||||
} | sed 's/%a/%A/g;s/%b/%B/g;s/%c/%C/g;s/%d/%D/g;s/%e/%E/g;s/%f/%F/g;s/%\(.\)a/%\1A/g;s/%\(.\)b/%\1B/g;s/%\(.\)c/%\1C/g;s/%\(.\)d/%\1D/g;s/%\(.\)e/%\1E/g;s/%\(.\)f/%\1F/g'
|
||||
}
|
||||
|
||||
_timestamp() {
|
||||
_ali_nonce() {
|
||||
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
|
||||
}
|
||||
|
||||
_ali_timestamp() {
|
||||
date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ"
|
||||
}
|
||||
|
||||
@@ -150,7 +167,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 +183,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 +199,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 +213,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'
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
dns_arubabusiness_info='ArubaBusiness
|
||||
Site: business.aruba.it
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_arubabusiness
|
||||
Options:
|
||||
AB_Key Your ArubaBusiness API Key
|
||||
AB_User Your account user
|
||||
AB_Pass Your account password
|
||||
'
|
||||
|
||||
#
|
||||
# A word of warning: as of this writing, api.arubabusiness.it only supports oauth authentication using the "password" grant type.
|
||||
# If you are REALLY sure you want to use it, it would be wise set up a dedicated technical user without administrative privileges
|
||||
#
|
||||
|
||||
ARUBABUSINESS_API='https://api.arubabusiness.it'
|
||||
|
||||
######## Public functions ########
|
||||
|
||||
#
|
||||
# Usage: dns_arubabusiness_add _acme-challenge.www.domain.com aaaabbbbcccc111122223333
|
||||
#
|
||||
# Add a new TXT record whose name and value match the given domain and value
|
||||
#
|
||||
# Variables
|
||||
# _full_domain: $1 - the name of the TXT record
|
||||
# _txt_value: $2 - the value of the TXT record
|
||||
# _body
|
||||
# dns_details
|
||||
# domain_id
|
||||
# dns_record_id
|
||||
# response
|
||||
#
|
||||
dns_arubabusiness_add() {
|
||||
_full_domain=$1
|
||||
_txt_value=$2
|
||||
|
||||
if ! _ab_authenticate; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_domain_id "$_full_domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
|
||||
# This is very unlikely, but allow the process to use the existing record
|
||||
_info "A TXT record with name: $_full_domain and value: $_txt_value already exists (id: $dns_record_id)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_body="{ \"IdDomain\": $domain_id, \"Type\": \"TXT\", \"Name\": \"$_full_domain\", \"Content\": \"\\\"$_txt_value\\\"\" }"
|
||||
|
||||
_debug "Adding TXT record with name: $_full_domain and value: $_txt_value"
|
||||
|
||||
if ! _ab_rest POST "api/domains/dns/record" "$_body" || ! _contains "$response" "DomainId"; then
|
||||
_err "Failed to add TXT record with name: $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Sleeping 10 seconds to let ArubaBusiness do its magic"
|
||||
_sleep 10
|
||||
|
||||
# Refresh dns details and check that the record was really added
|
||||
if ! _ab_dns_details "$root_domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
|
||||
# This should never happen
|
||||
_err "The TXT record with name: $_full_domain was not set"
|
||||
_err "Please check that the dns records are clean"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Added TXT record with id: $dns_record_id"
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: dns_arubabusiness_rm _acme-challenge.www.domain.com aaaabbbbcccc111122223333
|
||||
#
|
||||
# Remove the TXT record whose name and value match the given domain and value
|
||||
#
|
||||
# Variables
|
||||
# _full_domain: $1 - the name of the TXT record
|
||||
# _txt_value: $2 - the value of the TXT record
|
||||
# dns_details
|
||||
# dns_record_id
|
||||
#
|
||||
dns_arubabusiness_rm() {
|
||||
_full_domain=$1
|
||||
_txt_value=$2
|
||||
|
||||
if ! _ab_authenticate; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_domain_id "$_full_domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details" || [ -z "$dns_record_id" ]; then
|
||||
_err "Could not retrieve the record id for: $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Deleting TXT record: $dns_record_id"
|
||||
if ! _ab_rest DELETE "api/domains/dns/record/$dns_record_id" || ! _contains "$response" "DomainId"; then
|
||||
_err "Failed to delete TXT record: $dns_record_id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleted TXT record: $dns_record_id"
|
||||
return 0
|
||||
}
|
||||
|
||||
######## Private functions ########
|
||||
|
||||
#
|
||||
# Usage: _ab_domain_id _acme-challenge.www.domain.com
|
||||
#
|
||||
# Split the input domain into subdomain + root domain and get the id of the root domain
|
||||
#
|
||||
# Variables
|
||||
# _full_domain: $1 - the domain whose root needs to be extracted
|
||||
# _domain_sections
|
||||
# _current_index
|
||||
# _candidate_subdomain
|
||||
# _candidate_domain
|
||||
# sub_domain
|
||||
# root_domain
|
||||
# domain_id
|
||||
# dns_details: a json containing all dns records registered on the root domain
|
||||
#
|
||||
# Example
|
||||
# _get_root _acme-challenge.www.domain.com
|
||||
#
|
||||
# Should return
|
||||
# sub_domain=_acme-challenge.www
|
||||
# root_domain=domain.com
|
||||
# domain_id=123123123123
|
||||
# dns_details="{JSON_CONTENT}"
|
||||
#
|
||||
_ab_domain_id() {
|
||||
_full_domain=$1
|
||||
|
||||
_info "Attempting to retrieve root domain details for: $_full_domain"
|
||||
|
||||
_domain_sections=$(_math "$(printf "%s" "$_full_domain" | tr '.' '\n' | wc -l)" + 1)
|
||||
|
||||
if [ "$_domain_sections" -lt 1 ]; then
|
||||
_err "Invalid input $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_current_index=1
|
||||
while true; do
|
||||
_candidate_subdomain=$(if [ "$_current_index" = "1" ]; then printf ""; else printf "%s" "$_full_domain" | cut -d . -f 1-"$(_math "$_current_index" - 1)"; fi)
|
||||
_candidate_domain=$(printf "%s" "$_full_domain" | cut -d . -f "$_current_index"-"$_domain_sections")
|
||||
|
||||
if ! _ab_dns_details "$_candidate_domain"; then
|
||||
_debug2 "Could not fetch dns details for: $_candidate_domain"
|
||||
_current_index=$(_math "$_current_index" + 1)
|
||||
|
||||
# Fail if there are no candidates left
|
||||
if [ "$_current_index" -gt "$_domain_sections" ]; then
|
||||
_err "Could not determine the root domain for: $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
sub_domain="$_candidate_subdomain"
|
||||
root_domain="$_candidate_domain"
|
||||
# Extract the domain id, which is an integer and contains no commas
|
||||
domain_id="$(printf "%s" "$dns_details" | _egrep_o '"Id":[^,]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')"
|
||||
|
||||
if [ -z "$domain_id" ]; then
|
||||
_err "Could not determine the domain id for: $root_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Retrieved root domain id: $domain_id"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_dns_record_id _acme-challenge.www.domain.com "aaaabbbbcccc111122223333" "{JSON_CONTENT}"
|
||||
#
|
||||
# Extract the record id of the first TXT record whose name and content match the input values
|
||||
#
|
||||
# Variables
|
||||
# _record_name: $1
|
||||
# _txt_value: $2
|
||||
# _dns_details: $3 - the json returned by a previous call to '_ab_dns_details() $root_domain'
|
||||
# _record_ids
|
||||
# _record_names
|
||||
# _record_types
|
||||
# _record_contents
|
||||
# _record_ids_count
|
||||
# _record_names_count
|
||||
# _record_types_count
|
||||
# _record_contents_count
|
||||
# _i
|
||||
# dns_record_id
|
||||
#
|
||||
# Notes
|
||||
# TXT correspond to record type 5
|
||||
# ArubaBusiness appends a terminating dot (.) to the record name
|
||||
# The content field may contain the following character sequence: \"
|
||||
# All record names are always converted to lowercase
|
||||
#
|
||||
_ab_dns_record_id() {
|
||||
_record_name=$1
|
||||
_txt_value=$2
|
||||
_dns_details=$3
|
||||
|
||||
_record_name_lowercase=$(printf "%s" "$_record_name" | _lower_case)
|
||||
|
||||
# Extract the record ids, which are integers and contain no commas, colons or spaces
|
||||
# The first id is skipped because it refers to the domain id
|
||||
_record_ids=$(printf "%s" "$_dns_details" | sed 's/"Id":/\n"Id":/g' | _egrep_o '"Id":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')
|
||||
|
||||
# Extract the record names, which are strings but cannot contain commas, colons, spaces and quotes
|
||||
# The first name is skipped because it refers to the domain name
|
||||
_record_names=$(printf "%s" "$_dns_details" | sed 's/"Name":/\n"Name":/g' | _egrep_o '"Name":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' "' | tr '\n' ' ')
|
||||
|
||||
# Extract the record types, which are integers (except for the first one) and contain no commas, colons or spaces
|
||||
# The first type is skipped because it refers to the domain type
|
||||
_record_types=$(printf "%s" "$_dns_details" | sed 's/"Type":/\n"Type":/g' | _egrep_o '"Type":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')
|
||||
|
||||
# Extract the record contents, which are strings and may contain no quotes except for TXT records, which must be delimited by two \" literals
|
||||
# Note: There is no domain related entry here
|
||||
# Note: A " character is appended at the end of each content to make it easier to process the list later
|
||||
_record_contents=$(printf "%s" "$_dns_details" | sed 's/"Content":/\n"Content":/g' | sed 's/\\"//g' | _egrep_o '"Content": *"[^"]*"' | cut -d : -f 2- | sed -n 's/"\(.*\)"/\1/p' | tr '\n' '#')
|
||||
|
||||
_info "IDS: $_record_ids"
|
||||
_info "NAMES: $_record_names"
|
||||
_info "TYPEs: $_record_types"
|
||||
_info "CONTENTS: $_record_contents"
|
||||
|
||||
_record_ids_count=$(printf "%s" "$_record_ids" | tr ' ' '\n' | wc -l)
|
||||
_record_names_count=$(printf "%s" "$_record_names" | tr ' ' '\n' | wc -l)
|
||||
_record_types_count=$(printf "%s" "$_record_types" | tr ' ' '\n' | wc -l)
|
||||
_record_contents_count=$(printf "%s" "$_record_contents" | tr '#' '\n' | wc -l)
|
||||
|
||||
_info "Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
|
||||
|
||||
if [ "$_record_ids_count" != "$_record_names_count" ] || [ "$_record_ids_count" != "$_record_types_count" ] || [ "$_record_ids_count" != "$_record_contents_count" ]; then
|
||||
_err "Failed to parse record elements. Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Looking for a TXT record matching inputs - name: $_record_name_lowercase value: $_txt_value"
|
||||
|
||||
_i=1
|
||||
while [ "$_i" -le "$_record_ids_count" ]; do
|
||||
_current_name=$(printf "%s" "$_record_names" | cut -d " " -f "$_i")
|
||||
_current_type=$(printf "%s" "$_record_types" | cut -d " " -f "$_i")
|
||||
_current_content=$(printf "%s" "$_record_contents" | cut -d "#" -f "$_i")
|
||||
|
||||
if [ "$_record_name_lowercase." = "$_current_name" ] && [ "5" = "$_current_type" ] && [ "$_txt_value" = "$_current_content" ]; then
|
||||
dns_record_id=$(printf "%s" "$_record_ids" | cut -d " " -f "$_i")
|
||||
_info "Found matching record with id: $dns_record_id"
|
||||
return 0
|
||||
else
|
||||
_debug2 "Record does not match - type: '$_current_type' name: '$_current_name' value: '$_current_content'; Expected '$_record_name_lowercase.' '5' '$_txt_value'"
|
||||
fi
|
||||
_i=$(_math "$_i" + 1)
|
||||
done
|
||||
|
||||
_debug2 "No matching record was found in $_dns_details"
|
||||
return 1
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_dns_details domain.com
|
||||
#
|
||||
# Retrieve dns info for the given input domain
|
||||
#
|
||||
# Variables
|
||||
# _domain: $1
|
||||
# dns_details: the json returned by the call to $ARUBABUSINESS_API/api/domains/dns/$_domain/details (if return status is 0)
|
||||
# response
|
||||
#
|
||||
_ab_dns_details() {
|
||||
_domain=$1
|
||||
|
||||
if ! _ab_rest GET "api/domains/dns/$_domain/details" || ! _contains "$response" "DomainId"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
dns_details="$response"
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_authenticate
|
||||
#
|
||||
# Read account conf, update domain conf and perform user authentication to acquire an access token
|
||||
#
|
||||
# Variables
|
||||
# AB_Key
|
||||
# AB_User
|
||||
# AB_Pass
|
||||
# AB_Token
|
||||
#
|
||||
_ab_authenticate() {
|
||||
AB_Key="${AB_Key:-$(_readaccountconf_mutable AB_Key)}"
|
||||
AB_User="${AB_User:-$(_readaccountconf_mutable AB_User)}"
|
||||
AB_Pass="${AB_Pass:-$(_readaccountconf_mutable AB_Pass)}"
|
||||
|
||||
if [ -z "$AB_Key" ] || [ -z "$AB_User" ] || [ -z "$AB_Pass" ]; then
|
||||
AB_Key=""
|
||||
AB_User=""
|
||||
AB_Pass=""
|
||||
_err "Either the ArubaBusiness API key, the user or the password has not been defined yet."
|
||||
_err "Please configure them and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable AB_Key "$AB_Key"
|
||||
_saveaccountconf_mutable AB_User "$AB_User"
|
||||
_saveaccountconf_mutable AB_Pass "$AB_Pass"
|
||||
|
||||
if ! _ab_get_token || [ -z "$AB_Token" ]; then
|
||||
_err "Failed to acquire an access token"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_get_token
|
||||
#
|
||||
# Try acquiring a temporary access token. The token should have a 24h lifespan
|
||||
#
|
||||
# Variables
|
||||
# _ab_user_enc
|
||||
# _ab_pass_enc
|
||||
# _ab_authdata
|
||||
# AB_User
|
||||
# AB_Pass
|
||||
# AB_Token
|
||||
# response
|
||||
# _H2
|
||||
#
|
||||
_ab_get_token() {
|
||||
_ab_user_enc=$(printf "%s" "$AB_User" | _url_encode)
|
||||
_ab_pass_enc=$(printf "%s" "$AB_Pass" | _url_encode)
|
||||
_ab_authdata="grant_type=password&username=$_ab_user_enc&password=$_ab_pass_enc"
|
||||
|
||||
_H2="Content-Type: application/x-www-form-urlencoded"
|
||||
|
||||
if ! _ab_rest POST "auth/token" "$_ab_authdata" || ! _contains "$response" "access_token"; then
|
||||
_err "Authentication failure"
|
||||
return 1
|
||||
fi
|
||||
|
||||
AB_Token="$(printf "%s" "$response" | _egrep_o '"access_token":"[^\"]*"' | cut -d : -f 2 | tr -d '"')"
|
||||
|
||||
if [ -z "$AB_Token" ]; then
|
||||
_err "Could not extract access token"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Acquired access token"
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_rest POST "example/endpoint" "password=123"
|
||||
#
|
||||
# Perform a REST request using the given method, endpoint and data
|
||||
#
|
||||
# Variables
|
||||
# _method: $1 - The http method
|
||||
# _endpoint: $2 - The api path (relative to $ARUBABUSINESS_API)
|
||||
# _data: $3 - The body of the request (optional)
|
||||
# _key_trimmed
|
||||
# _token_trimmed
|
||||
# _ret_code
|
||||
# AB_Key
|
||||
# AB_Token
|
||||
# ARUBABUSINESS_API
|
||||
# _H1
|
||||
# _H2
|
||||
# _H3
|
||||
# _H4
|
||||
#
|
||||
_ab_rest() {
|
||||
_method=$1
|
||||
_endpoint="$2"
|
||||
_data="$3"
|
||||
|
||||
_key_trimmed=$(printf "%s" "$AB_Key" | tr -d '"')
|
||||
_token_trimmed=$(printf "%s" "$AB_Token" | tr -d '"')
|
||||
|
||||
_H1="Accept: application/json"
|
||||
|
||||
if [ -z "$_H2" ]; then
|
||||
# Default to application/json
|
||||
_H2="Content-Type: application/json"
|
||||
fi
|
||||
|
||||
if [ "$_key_trimmed" ]; then
|
||||
_H3="Authorization-Key: $_key_trimmed"
|
||||
else
|
||||
_err "Missing Api Key"
|
||||
_ab_cleanup_headers
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_token_trimmed" ]; then
|
||||
_H4="Authorization: Bearer $_token_trimmed"
|
||||
else
|
||||
_debug "No access token set"
|
||||
fi
|
||||
|
||||
if [ "$_method" != "GET" ]; then
|
||||
response="$(_post "$_data" "$ARUBABUSINESS_API/$_endpoint" "" "$_method")"
|
||||
else
|
||||
response="$(_get "$ARUBABUSINESS_API/$_endpoint")"
|
||||
fi
|
||||
|
||||
_ret_code=$?
|
||||
|
||||
if [ "$_ret_code" = "0" ] && _ab_call_is_success; then
|
||||
# Normalize the json response
|
||||
response="$(printf "%s" "$response" | _normalizeJson)"
|
||||
_ret_code=0
|
||||
else
|
||||
_err "Failed to call endpoint: $_endpoint"
|
||||
_ret_code=1
|
||||
fi
|
||||
|
||||
_ab_cleanup_headers
|
||||
|
||||
return $_ret_code
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_cleanup_headers
|
||||
#
|
||||
# Unset header variables to avoid interfering with other calls
|
||||
#
|
||||
# Variables
|
||||
# _H1
|
||||
# _H2
|
||||
# _H3
|
||||
# _H4
|
||||
#
|
||||
_ab_cleanup_headers() {
|
||||
# Cleanup request headers
|
||||
unset _H1 _H2 _H3 _H4 _H5
|
||||
|
||||
# Cleanup response headers
|
||||
if [ -f "$HTTP_HEADER" ]; then
|
||||
: >"$HTTP_HEADER"
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_call_is_success
|
||||
#
|
||||
# Check whether a call's response http status is one of 200, 201, 202 or 204 (other 2xx are not handled)
|
||||
#
|
||||
# Variables
|
||||
# _status
|
||||
# _http_status
|
||||
# _success_http_codes
|
||||
# HTTP_HEADER
|
||||
#
|
||||
_ab_call_is_success() {
|
||||
_success_http_codes="200 201 202 204"
|
||||
if [ -f "$HTTP_HEADER" ]; then
|
||||
_http_status=$(_egrep_o "^HTTP[\/0-9. ]*" <"$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)
|
||||
for _status in $_success_http_codes; do
|
||||
if [ "$_status" = "$_http_status" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
+10
-1
@@ -139,12 +139,21 @@ _get_autodns_zone() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Escape the XML special characters (& < > ' ") so that credentials
|
||||
# containing them do not break the request document (issue 5317).
|
||||
_autodns_xml_encode() {
|
||||
sed "s/&/\&/g;s/</\</g;s/>/\>/g;s/'/\'/g;s/\"/\"/g"
|
||||
}
|
||||
|
||||
_build_request_auth_xml() {
|
||||
_autodns_user_xml="$(printf "%s" "$AUTODNS_USER" | _autodns_xml_encode)"
|
||||
_autodns_password_xml="$(printf "%s" "$AUTODNS_PASSWORD" | _autodns_xml_encode)"
|
||||
_autodns_context_xml="$(printf "%s" "$AUTODNS_CONTEXT" | _autodns_xml_encode)"
|
||||
printf "<auth>
|
||||
<user>%s</user>
|
||||
<password>%s</password>
|
||||
<context>%s</context>
|
||||
</auth>" "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT"
|
||||
</auth>" "$_autodns_user_xml" "$_autodns_password_xml" "$_autodns_context_xml"
|
||||
}
|
||||
|
||||
# Arguments:
|
||||
|
||||
+3
-2
@@ -11,7 +11,8 @@ Options:
|
||||
# All `_sleep` commands are included to avoid Route53 throttling, see
|
||||
# https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests
|
||||
|
||||
AWS_HOST="route53.amazonaws.com"
|
||||
# Updated from "route53.amazonaws.com"
|
||||
AWS_HOST="route53.global.api.aws"
|
||||
AWS_URL="https://$AWS_HOST"
|
||||
|
||||
AWS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Amazon-Route53-API"
|
||||
@@ -161,7 +162,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
|
||||
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
#!/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_DNS_Host New DNS API host, default: dns.baidubce.com
|
||||
Baidu_API_Preference Engine preference, default: auto
|
||||
Baidu_BCD_Version API version number, default: 1
|
||||
Baidu_BCD_Expire Signature expiration seconds, default: 3600
|
||||
Baidu_View Resolve view, default: DEFAULT
|
||||
Baidu_Line New DNS line, 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"
|
||||
BAIDU_DNS_DEFAULT_HOST="dns.baidubce.com"
|
||||
|
||||
# --- Public API ---
|
||||
dns_baidu_add() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _baidu_run_with_fallback "add" "$fulldomain" "$txtvalue"; then
|
||||
_baidu_err "all baidu api engines failed for add: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
dns_baidu_rm() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _baidu_run_with_fallback "rm" "$fulldomain" "$txtvalue"; then
|
||||
_baidu_err "all baidu api engines failed for delete: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_baidu_run_with_fallback() {
|
||||
_action="$1"
|
||||
_fulldomain="$2"
|
||||
_txtvalue="$3"
|
||||
|
||||
if ! _baidu_load_credentials; then
|
||||
_baidu_err "baidu_load_credentials failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for _baidu_api_engine in $(_baidu_engine_order); do
|
||||
if ! _baidu_prepare_record "$_fulldomain"; then
|
||||
_baidu_info "prepare failed for engine: $_baidu_api_engine"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$_action" = "add" ]; then
|
||||
if _baidu_add_record "$_txtvalue"; then
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
if _baidu_rm_record "$_txtvalue"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
_baidu_info "engine failed, try next if available: $_baidu_api_engine"
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_baidu_engine_order() {
|
||||
_pref="$(_lower_case "$(_baidu_trim_ws "${Baidu_API_Preference:-auto}")")"
|
||||
case "$_pref" in
|
||||
legacy)
|
||||
printf "%s" "legacy new"
|
||||
;;
|
||||
new)
|
||||
printf "%s" "new legacy"
|
||||
;;
|
||||
*)
|
||||
printf "%s" "new legacy"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_baidu_add_record() {
|
||||
_txtvalue="$1"
|
||||
|
||||
if ! _baidu_find_record_ids_current "$_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
|
||||
|
||||
txtvalue="$(_baidu_trim_ws "$_txtvalue")"
|
||||
_record_domain="$(_baidu_trim_ws "$_record_domain")"
|
||||
_zone_name="$(_baidu_trim_ws "$_zone_name")"
|
||||
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
_line="$(_baidu_trim_ws "${Baidu_Line:-default}")"
|
||||
if [ -z "$_line" ]; then
|
||||
_line="default"
|
||||
fi
|
||||
_body="$(_baidu_payload_add_txt_dns "$_record_domain" "$txtvalue" "$_ttl" "$_line")"
|
||||
if ! _baidu_dns_call "POST" "/v1/dns/zone/${_zone_name}/record" "$_body"; then
|
||||
_baidu_err "baidu_dns_call failed: add record"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
_view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")"
|
||||
_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
|
||||
fi
|
||||
|
||||
if _baidu_is_api_error "$response"; then
|
||||
_baidu_err "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_baidu_rm_record() {
|
||||
_txtvalue="$1"
|
||||
|
||||
if ! _baidu_find_record_ids_current "$_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
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
if ! _baidu_dns_call "DELETE" "/v1/dns/zone/${_zone_name}/record/${_rid}" ""; then
|
||||
_baidu_err "baidu_dns_call failed: delete recordId=$_rid"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
_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
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$_baidu_api_engine" = "legacy" ]; then
|
||||
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
|
||||
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_DNS_HOST="${Baidu_DNS_Host:-$BAIDU_DNS_DEFAULT_HOST}"
|
||||
BAIDU_BCD_VERSION="${Baidu_BCD_Version:-1}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_baidu_prepare_record() {
|
||||
_fulldomain="$1"
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
if ! _baidu_get_root_dns "$_fulldomain"; then
|
||||
_baidu_err "Could not find zone by new dns api for $_fulldomain"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
if ! _baidu_get_root "$_fulldomain"; then
|
||||
_baidu_err "Could not find zone by legacy bcd api for $_fulldomain"
|
||||
return 1
|
||||
fi
|
||||
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_get_root_dns() {
|
||||
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_dns_call "GET" "/v1/dns/zone/${h}/record" ""; then
|
||||
_baidu_info "baidu_dns_call failed: list zones"
|
||||
elif ! _baidu_is_api_error "$response" && (_contains "$response" "\"records\"" || _contains "$response" "\"maxKeys\""); 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 by dns api: $_domain (host: $_sub_domain)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
}
|
||||
|
||||
_baidu_find_record_ids_current() {
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
_baidu_find_record_ids_dns "$@"
|
||||
else
|
||||
_baidu_find_record_ids "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
_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"
|
||||
}
|
||||
|
||||
_baidu_find_record_ids_dns() {
|
||||
_zone_name="$1"
|
||||
_record_domain="$2"
|
||||
_rdtype="$3"
|
||||
_rdata="$4"
|
||||
_BAIDU_FIND_RESULT=""
|
||||
|
||||
if ! _baidu_dns_call "GET" "/v1/dns/zone/${_zone_name}/record" ""; then
|
||||
_baidu_err "baidu_dns_call failed: list records"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _baidu_is_api_error "$response"; then
|
||||
_baidu_err "baidu_dns error: $(_baidu_json_get_str "$response" "code") $(_baidu_json_get_str "$response" "message")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_normalized="$(printf "%s" "$response" | _normalizeJson)"
|
||||
_records=$(printf "%s" "$_normalized" | sed 's/},{/}\n{/g')
|
||||
_ids=""
|
||||
|
||||
while IFS= read -r _line; do
|
||||
_id="$(_baidu_match_record_id_dns "$_line" "$_record_domain" "$_rdtype" "$_rdata")"
|
||||
if [ "$_id" ]; then
|
||||
_ids="$_ids $_id"
|
||||
fi
|
||||
done <<EOF
|
||||
$_records
|
||||
EOF
|
||||
|
||||
_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
|
||||
|
||||
_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")")"
|
||||
_H1="Authorization: $_auth"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_BCD_HOST"
|
||||
_H5=""
|
||||
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
|
||||
}
|
||||
|
||||
_baidu_dns_call() {
|
||||
_method="$1"
|
||||
_uri="$2"
|
||||
_payload="$3"
|
||||
_content_type="application/json"
|
||||
_attempt=1
|
||||
_max_attempts=3
|
||||
|
||||
while [ "$_attempt" -le "$_max_attempts" ]; do
|
||||
_ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
_payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)"
|
||||
|
||||
if ! _baidu_bce_auth "$_method" "$_uri" "" "$BAIDU_DNS_HOST" "$_ts" "${Baidu_BCD_Expire:-3600}" "$_content_type" "$_payload_hash"; then
|
||||
_baidu_err "baidu_dns auth failed"
|
||||
return 1
|
||||
fi
|
||||
_auth="$_BAIDU_BCE_AUTH_RESULT"
|
||||
_url="https://${BAIDU_DNS_HOST}${_uri}"
|
||||
|
||||
# Route through acme.sh's _get/_post (they honor _H1.._H5); no raw curl.
|
||||
_H1="Authorization: $_auth"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_DNS_HOST"
|
||||
_H5="Content-Type: $_content_type"
|
||||
|
||||
if [ "$_method" = "GET" ]; then
|
||||
response="$(_get "$_url")"
|
||||
elif [ "$_method" = "DELETE" ]; then
|
||||
response="$(_post "" "$_url" "" "DELETE")"
|
||||
else
|
||||
response="$(_post "$_payload" "$_url")"
|
||||
fi
|
||||
_ret="$?"
|
||||
_baidu_info "${_method} ${_uri} ret=${_ret}"
|
||||
|
||||
# Baidu may return a business error (Exception / 平台服务繁忙) inside HTTP 200.
|
||||
if [ "$_ret" = "0" ] && ! _contains "$response" "\"code\":\"Exception\"" && ! _contains "$response" "平台服务繁忙"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$_attempt" -lt "$_max_attempts" ]; then
|
||||
sleep 2
|
||||
fi
|
||||
_attempt=$(_math "$_attempt" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- 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_add_txt_dns() {
|
||||
_rr="$(printf "%s" "$1" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_value="$(printf "%s" "$2" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_ttl="$3"
|
||||
_line="$(printf "%s" "$4" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
printf "%s" "{\"rr\":\"${_rr}\",\"type\":\"TXT\",\"value\":\"${_value}\",\"ttl\":${_ttl},\"line\":\"${_line}\",\"description\":\"acme.sh\"}"
|
||||
}
|
||||
|
||||
_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_match_record_id_dns() {
|
||||
_line="$1"
|
||||
_rr="$(printf "%s" "$2" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_type="$(printf "%s" "$3" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_value="$(printf "%s" "$4" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
case "$_line" in
|
||||
*"\"rr\":\"${_rr}\""*"\"type\":\"${_type}\""*"\"value\":\"${_value}\""*)
|
||||
printf "%s" "$_line" | sed -n 's/.*"id":"\{0,1\}\([^",}]*\)"\{0,1\}.*/\1/p' | _head_n 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_baidu_hmac_sha256_hexkey() {
|
||||
_key_hex="$1"
|
||||
_msg="$2"
|
||||
printf "%s" "$_msg" | _hmac sha256 "$_key_hex" hex
|
||||
}
|
||||
Executable
+202
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_calrissia_info='Calrissia.be DNS API
|
||||
Site: calrissia.be
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_calrissia
|
||||
Options:
|
||||
CALRISSIA_TOKEN Personal access token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6809
|
||||
Author: Ward Hus
|
||||
'
|
||||
|
||||
CALRISSIA_API="https://my.calrissia.com/api"
|
||||
|
||||
dns_calrissia_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_calrissia_load_token || return 1
|
||||
|
||||
if ! _calrissia_get_root "$fulldomain"; then
|
||||
_err "Unable to find domain in Calrissia account for: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'"
|
||||
_info "Adding TXT record for $fulldomain"
|
||||
|
||||
_body="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120,\"prio\":0}"
|
||||
_response="$(_calrissia_request POST "/domain/$_domain_id/record" "$_body")"
|
||||
|
||||
if ! _contains "$_response" '"id"'; then
|
||||
_err "Failed to create TXT record: $_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
dns_calrissia_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_calrissia_load_token || return 1
|
||||
|
||||
if ! _calrissia_get_root "$fulldomain"; then
|
||||
_err "Unable to find domain in Calrissia account for: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'"
|
||||
|
||||
# Look the record up from the API instead of relying on local state.
|
||||
# The record list is embedded in the domain object.
|
||||
_response="$(_calrissia_request GET "/domain/$_domain_id")"
|
||||
_debug2 "Response: $_response"
|
||||
|
||||
# Split the record objects onto separate lines, then match on both the
|
||||
# subdomain name and the TXT value to find the record id to delete.
|
||||
_record_id="$(printf "%s" "$_response" |
|
||||
tr '{}' '\n' |
|
||||
grep "\"name\" *: *\"$_sub_domain\"" |
|
||||
grep "\"content\" *: *\"$txtvalue\"" |
|
||||
_egrep_o '"id" *: *[0-9]+' |
|
||||
_head_n 1 |
|
||||
_egrep_o '[0-9]+')"
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "No matching TXT record found for $fulldomain; nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "Removing TXT record id=$_record_id from domain id=$_domain_id"
|
||||
if ! _response="$(_calrissia_request DELETE "/domain/$_domain_id/record/$_record_id")" || _contains "$_response" '"error"'; then
|
||||
_err "Failed to remove TXT record: $_response"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
####################
|
||||
# Private helpers #
|
||||
####################
|
||||
|
||||
_calrissia_load_token() {
|
||||
CALRISSIA_TOKEN="${CALRISSIA_TOKEN:-$(_readaccountconf_mutable CALRISSIA_TOKEN)}"
|
||||
if [ -z "$CALRISSIA_TOKEN" ]; then
|
||||
_err "CALRISSIA_TOKEN is not set. Generate one at https://identity.calrissia.com under API Keys."
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable CALRISSIA_TOKEN "$CALRISSIA_TOKEN"
|
||||
}
|
||||
|
||||
# Sets _domain, _domain_id, _sub_domain for a given FQDN.
|
||||
_calrissia_get_root() {
|
||||
_fqdn="$1"
|
||||
|
||||
i=1
|
||||
while true; do
|
||||
_candidate="$(printf "%s" "$_fqdn" | cut -d . -f "$i"-)"
|
||||
[ -z "$_candidate" ] && return 1
|
||||
|
||||
_debug "Trying root domain: $_candidate"
|
||||
_response="$(_calrissia_request GET "/domain?full_domain_name=$_candidate")"
|
||||
_debug2 "Response: $_response"
|
||||
|
||||
_domain_id="$(printf "%s" "$_response" |
|
||||
_egrep_o '"id" *: *[0-9]+' |
|
||||
_head_n 1 |
|
||||
_egrep_o '[0-9]+')"
|
||||
|
||||
if [ -n "$_domain_id" ]; then
|
||||
if [ "$i" = "1" ]; then
|
||||
# The FQDN itself is the zone apex, e.g. a challenge-alias domain.
|
||||
_sub_domain=""
|
||||
else
|
||||
_sub_domain="$(printf "%s" "$_fqdn" | cut -d . -f "1-$((i - 1))")"
|
||||
fi
|
||||
_domain="$_candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
_calrissia_request() {
|
||||
_method="$1"
|
||||
_path="$2"
|
||||
_body="$3"
|
||||
export _H1="Authorization: Bearer $CALRISSIA_TOKEN"
|
||||
export _H2="Accept: application/json"
|
||||
if [ "$_method" = "GET" ]; then
|
||||
_get "$CALRISSIA_API$_path"
|
||||
else
|
||||
_post "$_body" "$CALRISSIA_API$_path" "" "$_method" "application/json"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
dns_cdmon_info='cdmon
|
||||
Site: www.cdmon.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cdmon
|
||||
Options:
|
||||
CDMON_Key API Key
|
||||
'
|
||||
|
||||
CDMON_Api="https://api-domains.cdmon.services/api-domains"
|
||||
|
||||
######## Public functions #####################
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
# Used to add txt record
|
||||
dns_cdmon_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
|
||||
|
||||
if [ -z "$CDMON_Key" ]; then
|
||||
CDMON_Key=""
|
||||
_err "You didn't specify your cdmon api key yet."
|
||||
_err "Please create your key and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable CDMON_Key "$CDMON_Key"
|
||||
|
||||
_debug "First, we detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
_info "Adding record"
|
||||
if _cdmon_rest "dnsrecords/create" "{\"data\":{\"type\":\"TXT\",\"domain\":\"$_domain\",\"value\":\"$txtvalue\",\"ttl\":120,\"host\":\"$_sub_domain\"}}"; then
|
||||
if _contains "$response" "\"status\":\"ok\""; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: fulldomain txtvalue
|
||||
# Used to remove the txt record after validation
|
||||
dns_cdmon_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
|
||||
_debug "First, we detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_info "Removing record"
|
||||
if _cdmon_rest "dnsrecords/delete" "{\"data\":{\"value\":\"$txtvalue\",\"type\":\"TXT\",\"domain\":\"$_domain\",\"host\":\"$_sub_domain\"}}"; then
|
||||
if _contains "$response" "\"status\":\"ok\""; then
|
||||
_info "Deleted, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Delete txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Delete txt record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
if ! _cdmon_rest "domains/list"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
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" "\"domain\":\"$h\""; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_cdmon_rest() {
|
||||
ep="$1"
|
||||
data="$2"
|
||||
_debug "$ep"
|
||||
|
||||
key_trimmed=$(echo "$CDMON_Key" | tr -d '"')
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
export _H2="apikey: $key_trimmed"
|
||||
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$CDMON_Api/$ep")"
|
||||
_ret="$?"
|
||||
|
||||
unset _H1 _H2
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "error $ep"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
@@ -135,7 +135,7 @@ _dns_cloudns_init_check() {
|
||||
_dns_cloudns_http_api_call "dns/login.json" ""
|
||||
|
||||
if ! _contains "$response" "\"status\":\"Success\""; then
|
||||
_err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Please check your login credentials."
|
||||
_err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Server response: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
+4
-2
@@ -15,7 +15,8 @@ CN_API="https://beta.api.core-networks.de"
|
||||
######## Public functions #####################
|
||||
|
||||
dns_cn_add() {
|
||||
fulldomain=$1
|
||||
# Core-Networks API requires punycode for IDN domains
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _cn_login; then
|
||||
@@ -58,7 +59,8 @@ dns_cn_add() {
|
||||
}
|
||||
|
||||
dns_cn_rm() {
|
||||
fulldomain=$1
|
||||
# Core-Networks API requires punycode for IDN domains
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _cn_login; then
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
dns_comlaude_info='comlaude.com
|
||||
Site: comlaude.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_comlaude
|
||||
Options:
|
||||
COMLAUDE_USERNAME User account
|
||||
COMLAUDE_PASSWORD User password
|
||||
COMLAUDE_API_KEY generated API key
|
||||
COMLAUDE_GROUP_ID Group ID in comlaude user profile
|
||||
Get it from the https://www.comlaude.com
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7112
|
||||
'
|
||||
# ===== CONFIG =====
|
||||
COMLAUDE_API="https://api.comlaude.com"
|
||||
|
||||
########## AUTH ##########
|
||||
|
||||
_comlaude_auth() {
|
||||
_debug "Checking cached ComLaude token"
|
||||
|
||||
# Try to get token from account.conf
|
||||
if [ -z "$COMLAUDE_ACCESS_TOKEN" ]; then
|
||||
COMLAUDE_ACCESS_TOKEN="$(_readaccountconf_mutable COMLAUDE_ACCESS_TOKEN)"
|
||||
COMLAUDE_TOKEN_EXPIRY="$(_readaccountconf_mutable COMLAUDE_TOKEN_EXPIRY)"
|
||||
fi
|
||||
|
||||
_now=$(_time)
|
||||
if [ -n "$COMLAUDE_ACCESS_TOKEN" ] && [ -n "$COMLAUDE_TOKEN_EXPIRY" ] && [ "$_now" -lt "$COMLAUDE_TOKEN_EXPIRY" ]; then
|
||||
_debug "Using cached ComLaude token (valid ${COMLAUDE_TOKEN_EXPIRY} > ${_now})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "ComLaude auth..."
|
||||
_comlaude_body="{\"username\":\"$COMLAUDE_USERNAME\",\"password\":\"$COMLAUDE_PASSWORD\",\"api_key\":\"$COMLAUDE_API_KEY\"}"
|
||||
_comlaude_response="$(_post "$_comlaude_body" "$COMLAUDE_API/api_login" "" "POST" "application/json")"
|
||||
|
||||
if ! _contains "$_comlaude_response" "access_token"; then
|
||||
_err "Auth failed: $_comlaude_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
COMLAUDE_ACCESS_TOKEN=$(echo "$_comlaude_response" | _egrep_o '"access_token":"[^"]*"' | cut -d'"' -f4)
|
||||
# store expiracy from api reply l'API ("expires_in" in seconds)
|
||||
_comlaude_expires_in=$(echo "$_comlaude_response" | _egrep_o '"expires_in":[0-9]*' | cut -d: -f2)
|
||||
[ -z "$_comlaude_expires_in" ] && _comlaude_expires_in=3000 # fallback if no info
|
||||
|
||||
COMLAUDE_TOKEN_EXPIRY=$(($(_time) + _comlaude_expires_in - 60)) # margin of 60s to secure renew
|
||||
|
||||
_saveaccountconf_mutable COMLAUDE_ACCESS_TOKEN "$COMLAUDE_ACCESS_TOKEN"
|
||||
_saveaccountconf_mutable COMLAUDE_TOKEN_EXPIRY "$COMLAUDE_TOKEN_EXPIRY"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
########## DOMAIN RESOLUTION ##########
|
||||
|
||||
_comlaude_get_root() {
|
||||
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
|
||||
if [ -z "$COMLAUDE_GROUP_ID" ]; then
|
||||
_err "Missing COMLAUDE_GROUP_ID"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_comlaude_input_domain="$1"
|
||||
_comlaude_input_domain="${_comlaude_input_domain#_acme-challenge.}"
|
||||
case "$_comlaude_input_domain" in
|
||||
\*.*) _comlaude_input_domain="${_comlaude_input_domain#*.}" ;;
|
||||
esac
|
||||
|
||||
_debug "Normalized domain: $_comlaude_input_domain"
|
||||
|
||||
_comlaude_i=1
|
||||
while true; do
|
||||
_comlaude_d=$(printf "%s" "$_comlaude_input_domain" | cut -d . -f "$_comlaude_i-")
|
||||
[ -z "$_comlaude_d" ] && {
|
||||
_debug "No matching domain found for $_comlaude_input_domain"
|
||||
return 1
|
||||
}
|
||||
|
||||
# don't test unnecessary levels
|
||||
# registered domain : TLD only (no dot after cut).
|
||||
case "$_comlaude_d" in
|
||||
*.*) : ;;
|
||||
*)
|
||||
_debug "Skipping bare TLD candidate: $_comlaude_d"
|
||||
_comlaude_i=$((_comlaude_i + 1))
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
_debug "Checking domain: $_comlaude_d"
|
||||
|
||||
_comlaude_retry=0
|
||||
_comlaude_max_retry=3 # to avoid network errors
|
||||
_comlaude_DOM_ID=""
|
||||
_comlaude_Z_ID=""
|
||||
|
||||
while [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ]; do
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
_debug "Full URL: $COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone"
|
||||
_comlaude_response="$(_get "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone")"
|
||||
_H1=""
|
||||
|
||||
_debug "RAW response for $_comlaude_d (try $((_comlaude_retry + 1))): $_comlaude_response"
|
||||
|
||||
# If empty -> true network issue, we retry
|
||||
if [ -z "$_comlaude_response" ]; then
|
||||
_comlaude_retry=$((_comlaude_retry + 1))
|
||||
[ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
# 404 -> domain not found in that level. no retry : continue
|
||||
if echo "$_comlaude_response" | grep -q '"status_code":404'; then
|
||||
_debug "404 for $_comlaude_d, moving to next level (not retrying)"
|
||||
break
|
||||
fi
|
||||
|
||||
# Domain missing (200 reply, data empty) -> continue
|
||||
if echo "$_comlaude_response" | grep -q '"data":\[\]'; then
|
||||
_debug "Empty data for $_comlaude_d, moving to next level"
|
||||
break
|
||||
fi
|
||||
|
||||
# Extraction via _egrep_o
|
||||
_comlaude_DOM_ID="$(echo "$_comlaude_response" | _egrep_o '"id":"[^"]*"' | head -n1 | cut -d':' -f2 | tr -d '"')"
|
||||
_comlaude_Z_ID="$(echo "$_comlaude_response" | _egrep_o '"active_zone":\{"id":"[^"]*"' | _egrep_o '"id":"[^"]*"$' | cut -d':' -f2 | tr -d '"')"
|
||||
|
||||
if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# 200 reply but malformed data / noid -> retry transport
|
||||
_comlaude_retry=$((_comlaude_retry + 1))
|
||||
[ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2
|
||||
done
|
||||
|
||||
_debug "_comlaude_DOM_ID=$_comlaude_DOM_ID"
|
||||
_debug "_comlaude_Z_ID=$_comlaude_Z_ID"
|
||||
|
||||
if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then
|
||||
_comlaude_domain="$_comlaude_d"
|
||||
_comlaude_domain_id="$_comlaude_DOM_ID"
|
||||
_comlaude_zone_id="$_comlaude_Z_ID"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_comlaude_i=$((_comlaude_i + 1))
|
||||
done
|
||||
}
|
||||
########## ADD TXT ##########
|
||||
|
||||
dns_comlaude_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}"
|
||||
COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}"
|
||||
COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}"
|
||||
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
|
||||
|
||||
if [ -z "$COMLAUDE_USERNAME" ] || [ -z "$COMLAUDE_PASSWORD" ] || [ -z "$COMLAUDE_API_KEY" ]; then
|
||||
_err "You didn't specify ComLaude credentials (COMLAUDE_USERNAME, COMLAUDE_PASSWORD, COMLAUDE_API_KEY)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Backup variable after validation
|
||||
_saveaccountconf_mutable COMLAUDE_USERNAME "$COMLAUDE_USERNAME"
|
||||
_saveaccountconf_mutable COMLAUDE_PASSWORD "$COMLAUDE_PASSWORD"
|
||||
_saveaccountconf_mutable COMLAUDE_API_KEY "$COMLAUDE_API_KEY"
|
||||
_saveaccountconf_mutable COMLAUDE_GROUP_ID "$COMLAUDE_GROUP_ID"
|
||||
|
||||
_info "Adding TXT: $fulldomain"
|
||||
_comlaude_auth || return 1
|
||||
_comlaude_get_root "$fulldomain" || return 1
|
||||
|
||||
_debug "Root: $_comlaude_domain"
|
||||
|
||||
_comlaude_data="{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"value\":\"$txtvalue\",\"ttl\":60}"
|
||||
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
export _H2="Content-Type: application/json"
|
||||
|
||||
_comlaude_response="$(_post "$_comlaude_data" "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records")"
|
||||
|
||||
_H1=""
|
||||
_H2=""
|
||||
if ! echo "$_comlaude_response" | grep -q '"id"'; then
|
||||
_err "Failed to create TXT"
|
||||
_debug "$_comlaude_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
########## REMOVE TXT ##########
|
||||
|
||||
dns_comlaude_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}"
|
||||
COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}"
|
||||
COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}"
|
||||
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
|
||||
|
||||
_info "Removing TXT: $fulldomain"
|
||||
|
||||
_comlaude_auth || return 1
|
||||
_comlaude_get_root "$fulldomain" || return 1
|
||||
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
_comlaude_encoded_name="$(printf '%s' "$fulldomain" | _url_encode)"
|
||||
_comlaude_encoded_value="$(printf '%s' "$txtvalue" | _url_encode)"
|
||||
_comlaude_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records?filter[type]=TXT&filter[name]=$_comlaude_encoded_name&filter[value]=$_comlaude_encoded_value"
|
||||
_comlaude_response="$(_get "$_comlaude_url")"
|
||||
_H1=""
|
||||
|
||||
_debug "Filtered records response: $_comlaude_response"
|
||||
|
||||
# first "id" top-level of reply (record itself,
|
||||
# always on first position of each data[] object)
|
||||
_comlaude_record_id="$(echo "$_comlaude_response" | _egrep_o '"data":\[\{"id":"[^"]*"' | _egrep_o '"[^"]*"$' | tr -d '"')"
|
||||
|
||||
if [ -z "$_comlaude_record_id" ]; then
|
||||
_info "No matching TXT record found to delete for $fulldomain / $txtvalue"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "Deleting record $_comlaude_record_id"
|
||||
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
_comlaude_del_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records/$_comlaude_record_id"
|
||||
_comlaude_del_resp="$(_post "" "$_comlaude_del_url" "" "DELETE")"
|
||||
_H1=""
|
||||
|
||||
if echo "$_comlaude_del_resp" | grep -q '"error"'; then
|
||||
_err "Delete failed for $_comlaude_record_id"
|
||||
_debug "$_comlaude_del_resp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleted record $_comlaude_record_id"
|
||||
return 0
|
||||
}
|
||||
+21
-7
@@ -38,7 +38,7 @@ dns_cpanel_add() {
|
||||
fi
|
||||
# adding entry
|
||||
_info "Adding the entry"
|
||||
stripped_fulldomain=$(echo "$fulldomain" | sed "s/.$_domain//")
|
||||
stripped_fulldomain="${fulldomain%."$_domain"}"
|
||||
_debug "Adding $stripped_fulldomain to $_domain zone"
|
||||
_myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=add_zone_record&domain=$_domain&name=$stripped_fulldomain&type=TXT&txtdata=$txtvalue&ttl=1"
|
||||
if _successful_update; then return 0; fi
|
||||
@@ -128,13 +128,27 @@ _get_root() {
|
||||
_err "Primary domain list not found!"
|
||||
return 1
|
||||
fi
|
||||
for _domain in $_domains; do
|
||||
_debug "Checking if $fulldomain ends with $_domain"
|
||||
if (_endswith "$fulldomain" "$_domain"); then
|
||||
_debug "Root domain: $_domain"
|
||||
return 0
|
||||
fi
|
||||
# Pick the LONGEST matching zone, dot-anchored: with both domain.tld and
|
||||
# sub.domain.tld zones on the account, cPanel stores the record in the
|
||||
# most specific zone, so add and rm must both resolve to that one.
|
||||
_domain=""
|
||||
for d in $_domains; do
|
||||
_debug "Checking if $fulldomain ends with $d"
|
||||
# case with quoted patterns gives an exact literal suffix match;
|
||||
# _endswith treats the needle as a regex, so its dots would let
|
||||
# xdomain.tld wrongly match zone domain.tld
|
||||
case "$fulldomain" in
|
||||
"$d" | *".$d")
|
||||
if [ "${#d}" -gt "${#_domain}" ]; then
|
||||
_domain="$d"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [ -n "$_domain" ]; then
|
||||
_debug "Root domain: $_domain"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
Executable
+269
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_creoline_info='creoline
|
||||
Site: https://www.creoline.com/de
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_creoline
|
||||
Help: https://help.creoline.com
|
||||
Options:
|
||||
creolineApiToken
|
||||
creolineApiSecret
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7103
|
||||
'
|
||||
|
||||
creolineApi="https://api.creoline.com/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPB8"
|
||||
dns_creoline_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}"
|
||||
creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}"
|
||||
|
||||
if [ -z "$creolineApiToken" ] || [ -z "$creolineApiSecret" ]; then
|
||||
_err "Error required creoline API Token or creoline API Secret not specified."
|
||||
_err "Please set it with the Command 'export creolineApiToken=<YourToken>' and 'export creolineApiSecret=<YourSecret>'."
|
||||
return 1
|
||||
else
|
||||
_saveaccountconf_mutable creolineApiToken "$creolineApiToken"
|
||||
_saveaccountconf_mutable creolineApiSecret "$creolineApiSecret"
|
||||
fi
|
||||
|
||||
_debug "Detecting the root dns zone."
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Error on detecting the root dns zone."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Adding record"
|
||||
if _creoline_rest POST "dns/zone/$_domain/record" "{\"type\":\"TXT\",\"host\":\"$_sub_domain\",\"record\":\"$txtvalue\",\"ttl\":\"60\"}"; then
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
#fulldomain txtvalue
|
||||
dns_creoline_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}"
|
||||
creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}"
|
||||
|
||||
_debug "Detecting the root dns zone."
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Error on detecting the root dns zone."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Getting earlier created txt record."
|
||||
if ! _creoline_rest GET "dns/zone/$_domain/record/type/TXT/record/$txtvalue"; then
|
||||
if _contains "$response" "errors" || _contains "$response" "message"; then
|
||||
_err "Error on getting earlier created txt record."
|
||||
return 1
|
||||
fi
|
||||
_err "Error on getting earlier created txt record."
|
||||
return 1
|
||||
fi
|
||||
|
||||
record_id=$(echo "$response" | _egrep_o "\"id\"[[:space:]]*:[[:space:]]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||
_debug "record_id" "$record_id"
|
||||
|
||||
if [ -z "$record_id" ]; then
|
||||
_err "Error on deleting earlier created txt record. No record id found in response."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleting earlier created txt record."
|
||||
if ! _creoline_rest DELETE "dns/zone/$_domain/record/$record_id"; then
|
||||
if _contains "$response" "errors" || _contains "$response" "message"; then
|
||||
_err "Error on deleting earlier created txt record."
|
||||
return 1
|
||||
fi
|
||||
_err "Error on deleting earlier created txt record."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleted, OK"
|
||||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
if ! _creoline_rest GET "dns/zone/root/$domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_domain=$(echo "$response" | _egrep_o "\"domain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
_creoline_rest() {
|
||||
method=$1
|
||||
uri="$2"
|
||||
data="$3"
|
||||
timestamp=$(_time)
|
||||
canonical_request="${timestamp}.${creolineApi}/${uri}"
|
||||
signature_hash=$(printf "%s" "$canonical_request" | _hmac sha256 "$(printf "%s" "$creolineApiSecret" | _hex_dump | tr -d " ")" hex)
|
||||
|
||||
_debug method "$method"
|
||||
_debug uri "$uri"
|
||||
_debug data "$data"
|
||||
|
||||
_debug2 timestamp "$timestamp"
|
||||
_debug2 canonical_request "$canonical_request"
|
||||
_debug2 signature_hash "$signature_hash"
|
||||
|
||||
token_trimmed=$(echo "$creolineApiToken" | tr -d '"')
|
||||
hmac_trimmed=$(echo "$signature_hash" | tr -d '"')
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
|
||||
if [ "$token_trimmed" ]; then
|
||||
export _H2="X-Api-Token: $token_trimmed"
|
||||
fi
|
||||
|
||||
if [ "$hmac_trimmed" ]; then
|
||||
export _H3="X-Creoline-Api-Signature: $hmac_trimmed"
|
||||
fi
|
||||
|
||||
if [ "$timestamp" ]; then
|
||||
export _H4="X-Creoline-Api-Timestamp: $timestamp"
|
||||
fi
|
||||
|
||||
if [ "$method" != "GET" ]; then
|
||||
response="$(_post "$data" "$creolineApi/$uri" "" "$method")"
|
||||
else
|
||||
response="$(_get "$creolineApi/$uri")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "error $uri"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug response "$response"
|
||||
|
||||
if _contains "$response" "errors"; then
|
||||
error=$(echo "$response" | _egrep_o "\"errors\":[[]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | tr -d "[")
|
||||
_err "Error: $error"
|
||||
_err "URI:$uri"
|
||||
return 1
|
||||
elif _contains "$response" "message"; then
|
||||
message=$(echo "$response" | _egrep_o "\"message\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \")
|
||||
_err "Error: $message"
|
||||
_err "URI:$uri"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
+34
-6
@@ -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() {
|
||||
|
||||
@@ -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="$(printf '%s' "$_res" | _normalizeJson)"
|
||||
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"
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ dns_da_info='DirectAdmin Server API
|
||||
Site: DirectAdmin.com/api.php
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_da
|
||||
Options:
|
||||
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443"
|
||||
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443". Special characters in the user/password must be percent-encoded, e.g. "@" -> "%40".
|
||||
DA_Api_Insecure Insecure TLS. 0: check for cert validity, 1: always accept
|
||||
Issues: github.com/TigerP/acme.sh/issues
|
||||
'
|
||||
|
||||
+7
-4
@@ -4,7 +4,7 @@ dns_desec_info='deSEC.io
|
||||
Site: desec.readthedocs.io/en/latest/
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_desec
|
||||
Options:
|
||||
DDNSS_Token API Token
|
||||
DEDYN_TOKEN API Token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2180
|
||||
Author: Zheng Qian
|
||||
'
|
||||
@@ -39,6 +39,7 @@ dns_desec_add() {
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_sub_domain=$(echo "$_sub_domain" | _lower_case)
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
@@ -48,7 +49,7 @@ dns_desec_add() {
|
||||
_desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/"
|
||||
|
||||
if [ "$_code" = "200" ]; then
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
_debug "existing TXT found"
|
||||
_debug oldtxtvalues "$oldtxtvalues"
|
||||
if [ -n "$oldtxtvalues" ]; then
|
||||
@@ -100,7 +101,7 @@ dns_desec_rm() {
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_sub_domain=$(echo "$_sub_domain" | _lower_case)
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
@@ -110,7 +111,7 @@ dns_desec_rm() {
|
||||
_desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/"
|
||||
|
||||
if [ "$_code" = "200" ]; then
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
_debug "existing TXT found"
|
||||
_debug oldtxtvalues "$oldtxtvalues"
|
||||
if [ -n "$oldtxtvalues" ]; then
|
||||
@@ -150,6 +151,8 @@ _desec_rest() {
|
||||
if [ "$m" != "GET" ]; then
|
||||
_secure_debug2 data "$data"
|
||||
response="$(_post "$data" "$ep" "" "$m")"
|
||||
_info "Sleeping 1s to respect deSEC write rate limit"
|
||||
_sleep 1
|
||||
else
|
||||
response="$(_get "$ep")"
|
||||
fi
|
||||
|
||||
+25
-90
@@ -5,14 +5,11 @@ Site: DNSExit.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsexit
|
||||
Options:
|
||||
DNSEXIT_API_KEY API Key
|
||||
DNSEXIT_AUTH_USER Username
|
||||
DNSEXIT_AUTH_PASS Password
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/4719
|
||||
Author: Samuel Jimenez
|
||||
'
|
||||
|
||||
DNSEXIT_API_URL="https://api.dnsexit.com/dns/"
|
||||
DNSEXIT_HOSTS_URL="https://update.dnsexit.com/ipupdate/hosts.jsp"
|
||||
|
||||
######## Public functions #####################
|
||||
#Usage: dns_dnsexit_add _acme-challenge.*.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
@@ -28,20 +25,7 @@ dns_dnsexit_add() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug 'First detect the root zone'
|
||||
if ! _get_root "$fulldomain"; then
|
||||
return 1
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"add\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":0,\"overwrite\":false}}"; then
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 _response "$response"
|
||||
return 0
|
||||
_dnsexit_zone_op add ',"ttl":1,"overwrite":false'
|
||||
}
|
||||
|
||||
#Usage: fulldomain txtvalue
|
||||
@@ -58,54 +42,43 @@ dns_dnsexit_rm() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug 'First detect the root zone'
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"delete\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"}}"; then
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 _response "$response"
|
||||
return 0
|
||||
_dnsexit_zone_op delete ''
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
# The legacy zone-detection endpoint (update.dnsexit.com/ipupdate/hosts.jsp)
|
||||
# was shut down by DNSExit and now returns 503, and the JSON API offers no
|
||||
# zone-list call. So find the root zone by attempting the actual operation at
|
||||
# each domain level: the API answers "code":0 only when the domain matches a
|
||||
# zone of the account. https://github.com/acmesh-official/acme.sh/issues/6914
|
||||
#Usage: _dnsexit_zone_op <add|delete> <extra-json-fields>
|
||||
_dnsexit_zone_op() {
|
||||
_op="$1"
|
||||
_extra="$2"
|
||||
i=1
|
||||
while true; do
|
||||
_domain=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$_domain"
|
||||
_domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
|
||||
_debug _domain "$_domain"
|
||||
if [ -z "$_domain" ]; then
|
||||
_err "Could not find the root zone of $fulldomain in your DNSExit account"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug login "$DNSEXIT_AUTH_USER"
|
||||
_debug password "$DNSEXIT_AUTH_PASS"
|
||||
_debug domain "$_domain"
|
||||
_sub_domain="$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//")"
|
||||
if [ "$_sub_domain" = "$fulldomain" ]; then
|
||||
_sub_domain=""
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_dnsexit_http "login=$DNSEXIT_AUTH_USER&password=$DNSEXIT_AUTH_PASS&domain=$_domain"
|
||||
|
||||
if _contains "$response" "0=$_domain"; then
|
||||
_sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")"
|
||||
return 0
|
||||
else
|
||||
_debug "Go to next level of $_domain"
|
||||
if _dnsexit_rest "{\"domain\":\"$_domain\",\"$_op\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"$_extra}}"; then
|
||||
if _contains "$response" "\"code\":0" || _contains "$response" "\"code\": 0"; then
|
||||
_debug2 _response "$response"
|
||||
return 0
|
||||
fi
|
||||
_debug "Zone $_domain was not accepted, trying the next level" "$response"
|
||||
fi
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_dnsexit_rest() {
|
||||
@@ -136,27 +109,7 @@ _dnsexit_rest() {
|
||||
return 0
|
||||
}
|
||||
|
||||
_dnsexit_http() {
|
||||
m=GET
|
||||
param="$1"
|
||||
_debug param "$param"
|
||||
_debug get "$DNSEXIT_HOSTS_URL?$param"
|
||||
|
||||
response="$(_get "$DNSEXIT_HOSTS_URL?$param")"
|
||||
|
||||
_debug response "$response"
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Error $param"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_account_info() {
|
||||
|
||||
DNSEXIT_API_KEY="${DNSEXIT_API_KEY:-$(_readaccountconf_mutable DNSEXIT_API_KEY)}"
|
||||
if test -z "$DNSEXIT_API_KEY"; then
|
||||
DNSEXIT_API_KEY=''
|
||||
@@ -166,23 +119,5 @@ get_account_info() {
|
||||
|
||||
_saveaccountconf_mutable DNSEXIT_API_KEY "$DNSEXIT_API_KEY"
|
||||
|
||||
DNSEXIT_AUTH_USER="${DNSEXIT_AUTH_USER:-$(_readaccountconf_mutable DNSEXIT_AUTH_USER)}"
|
||||
if test -z "$DNSEXIT_AUTH_USER"; then
|
||||
DNSEXIT_AUTH_USER=""
|
||||
_err 'DNSEXIT_AUTH_USER was not exported'
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable DNSEXIT_AUTH_USER "$DNSEXIT_AUTH_USER"
|
||||
|
||||
DNSEXIT_AUTH_PASS="${DNSEXIT_AUTH_PASS:-$(_readaccountconf_mutable DNSEXIT_AUTH_PASS)}"
|
||||
if test -z "$DNSEXIT_AUTH_PASS"; then
|
||||
DNSEXIT_AUTH_PASS=""
|
||||
_err 'DNSEXIT_AUTH_PASS was not exported'
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable DNSEXIT_AUTH_PASS "$DNSEXIT_AUTH_PASS"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
+37
-8
@@ -5,6 +5,7 @@ Site: DNSimple.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple
|
||||
Options:
|
||||
DNSimple_OAUTH_TOKEN OAuth Token
|
||||
DNSimple_ACCOUNT_ID Account ID. Optional, only needed when the token can access multiple accounts.
|
||||
Issues: github.com/pho3nixf1re/acme.sh/issues
|
||||
'
|
||||
|
||||
@@ -17,6 +18,7 @@ dns_dnsimple_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
|
||||
DNSimple_OAUTH_TOKEN=""
|
||||
_err "You have not set the dnsimple oauth token yet."
|
||||
@@ -25,10 +27,10 @@ dns_dnsimple_add() {
|
||||
fi
|
||||
|
||||
# save the oauth token for later
|
||||
_saveaccountconf DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
|
||||
_saveaccountconf_mutable DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
|
||||
|
||||
if ! _get_account_id; then
|
||||
_err "failed to retrive account id"
|
||||
_err "failed to retrieve account id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -56,8 +58,14 @@ dns_dnsimple_add() {
|
||||
dns_dnsimple_rm() {
|
||||
fulldomain=$1
|
||||
|
||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
|
||||
_err "You have not set the dnsimple oauth token yet."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _get_account_id; then
|
||||
_err "failed to retrive account id"
|
||||
_err "failed to retrieve account id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -122,13 +130,16 @@ _get_root() {
|
||||
|
||||
# returns _account_id
|
||||
_get_account_id() {
|
||||
_debug "retrive account id"
|
||||
if ! _dnsimple_rest GET "whoami"; then
|
||||
return 1
|
||||
DNSimple_ACCOUNT_ID="${DNSimple_ACCOUNT_ID:-$(_readaccountconf_mutable DNSimple_ACCOUNT_ID)}"
|
||||
if [ "$DNSimple_ACCOUNT_ID" ]; then
|
||||
_saveaccountconf_mutable DNSimple_ACCOUNT_ID "$DNSimple_ACCOUNT_ID"
|
||||
_account_id="$DNSimple_ACCOUNT_ID"
|
||||
_debug _account_id "$_account_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"account\":null"; then
|
||||
_err "no account associated with this token"
|
||||
_debug "retrieve account id"
|
||||
if ! _dnsimple_rest GET "whoami"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -137,7 +148,25 @@ _get_account_id() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"account\":null"; then
|
||||
# the whoami of a user token (dnsimple_u_*) carries no account,
|
||||
# so list the accounts the token can access instead
|
||||
# https://github.com/acmesh-official/acme.sh/issues/6491
|
||||
if ! _dnsimple_rest GET "accounts"; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_account_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"email\":" | cut -d: -f2 | cut -d, -f1)
|
||||
if [ -z "$_account_id" ]; then
|
||||
_err "no account associated with this token"
|
||||
return 1
|
||||
fi
|
||||
if [ "$(echo "$_account_id" | wc -l)" -gt 1 ]; then
|
||||
_err "The token has access to multiple accounts, please pick one and set it explicitly:"
|
||||
_err "export DNSimple_ACCOUNT_ID=<one of: $(echo "$_account_id" | tr '\n' ' ')>"
|
||||
return 1
|
||||
fi
|
||||
_debug _account_id "$_account_id"
|
||||
|
||||
return 0
|
||||
|
||||
+10
-6
@@ -23,6 +23,8 @@ dns_dynu_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}"
|
||||
Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}"
|
||||
if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then
|
||||
Dynu_ClientId=""
|
||||
Dynu_Secret=""
|
||||
@@ -32,8 +34,8 @@ dns_dynu_add() {
|
||||
fi
|
||||
|
||||
#save the client id and secret to the account conf file.
|
||||
_saveaccountconf Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf Dynu_Secret "$Dynu_Secret"
|
||||
_saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf_mutable Dynu_Secret "$Dynu_Secret"
|
||||
|
||||
if [ -z "$Dynu_Token" ]; then
|
||||
_info "Getting Dynu token."
|
||||
@@ -69,6 +71,8 @@ dns_dynu_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}"
|
||||
Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}"
|
||||
if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then
|
||||
Dynu_ClientId=""
|
||||
Dynu_Secret=""
|
||||
@@ -78,8 +82,8 @@ dns_dynu_rm() {
|
||||
fi
|
||||
|
||||
#save the client id and secret to the account conf file.
|
||||
_saveaccountconf Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf Dynu_Secret "$Dynu_Secret"
|
||||
_saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf_mutable Dynu_Secret "$Dynu_Secret"
|
||||
|
||||
if [ -z "$Dynu_Token" ]; then
|
||||
_info "Getting Dynu token."
|
||||
@@ -214,11 +218,11 @@ _dynu_authentication() {
|
||||
|
||||
response="$(_get "$Dynu_EndPoint/oauth2/token")"
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Authentication failed."
|
||||
_err "Authentication failed: no response from $Dynu_EndPoint/oauth2/token"
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "Authentication Exception"; then
|
||||
_err "Authentication failed."
|
||||
_err "Authentication failed. Server response: $response"
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "access_token"; then
|
||||
|
||||
+6
-8
@@ -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
|
||||
|
||||
+6
-11
@@ -363,17 +363,12 @@ _edgedns_rest() {
|
||||
|
||||
_edgedns_eg_timestamp() {
|
||||
_debug "Generating signature Timestamp"
|
||||
_debug3 "Retriving ntp time"
|
||||
_timeheaders="$(_get "https://www.ntp.org" "onlyheader")"
|
||||
_debug3 "_timeheaders" "$_timeheaders"
|
||||
_ntpdate="$(echo "$_timeheaders" | grep -i "Date:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n")"
|
||||
_debug3 "_ntpdate" "$_ntpdate"
|
||||
_ntpdate="$(echo "${_ntpdate}" | sed -e 's/^[[:space:]]*//')"
|
||||
_debug3 "_NTPDATE" "$_ntpdate"
|
||||
_ntptime="$(echo "${_ntpdate}" | _head_n 1 | cut -d " " -f 5 | tr -d "\r\n")"
|
||||
_debug3 "_ntptime" "$_ntptime"
|
||||
_eg_timestamp=$(date -u "+%Y%m%dT")
|
||||
_eg_timestamp="$(printf "%s%s+0000" "$_eg_timestamp" "$_ntptime")"
|
||||
#Akamai accepts a clock skew of +/-30s, so use the system clock directly.
|
||||
#The previous code fetched the Date header from www.ntp.org, which is not
|
||||
#a reliable time source (it served a wrong time for hours, issue 3973),
|
||||
#cost an extra https round-trip for every API request, and combined the
|
||||
#remote time of day with the LOCAL date, breaking around UTC midnight.
|
||||
_eg_timestamp="$(date -u "+%Y%m%dT%H:%M:%S+0000")"
|
||||
_debug "_eg_timestamp" "$_eg_timestamp"
|
||||
}
|
||||
|
||||
|
||||
Executable
+139
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_eurodns_info='EuroDNS
|
||||
Site: eurodns.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_eurodns
|
||||
Options:
|
||||
EURODNS_APP_ID Application ID
|
||||
EURODNS_API_KEY API Key
|
||||
EURODNS_TTL TTL. Default: "600".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues
|
||||
Author: Nicolas Santorelli
|
||||
'
|
||||
|
||||
#
|
||||
# EuroDNS DNS API
|
||||
#
|
||||
# EuroDNS API documentation:
|
||||
# https://docapi.eurodns.com
|
||||
#
|
||||
# Usage:
|
||||
# export EURODNS_APP_ID="your-app-id"
|
||||
# export EURODNS_API_KEY="your-api-key"
|
||||
# acme.sh --issue --dns dns_eurodns -d example.com -d *.example.com
|
||||
#
|
||||
# The credentials will be saved in ~/.acme.sh/account.conf
|
||||
#
|
||||
# Optional:
|
||||
# export EURODNS_API_URL="https://rest-api.eurodns.com" # Default API URL
|
||||
# export EURODNS_TTL=600 # Default TTL (minimum 600 for EuroDNS)
|
||||
#
|
||||
|
||||
EURODNS_API_DEFAULT="https://rest-api.eurodns.com"
|
||||
EURODNS_TTL_DEFAULT=600
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#Usage: dns_eurodns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_eurodns_add() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
_info "Using EuroDNS DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
|
||||
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
|
||||
EURODNS_TTL="${EURODNS_TTL:-$(_readaccountconf_mutable EURODNS_TTL)}"
|
||||
EURODNS_TTL="${EURODNS_TTL:-$EURODNS_TTL_DEFAULT}"
|
||||
|
||||
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
|
||||
EURODNS_APP_ID=""
|
||||
EURODNS_API_KEY=""
|
||||
_err "You didn't specify EuroDNS App ID and API Key."
|
||||
_err "Please export EURODNS_APP_ID and EURODNS_API_KEY and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable EURODNS_APP_ID "$EURODNS_APP_ID"
|
||||
_saveaccountconf_mutable EURODNS_API_KEY "$EURODNS_API_KEY"
|
||||
if [ "$EURODNS_API_URL" != "$EURODNS_API_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable EURODNS_API_URL "$EURODNS_API_URL"
|
||||
fi
|
||||
if [ "$EURODNS_TTL" != "$EURODNS_TTL_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable EURODNS_TTL "$EURODNS_TTL"
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_info "Adding TXT record"
|
||||
if _eurodns_add_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
|
||||
_info "Added TXT record successfully."
|
||||
return 0
|
||||
else
|
||||
_err "Failed to add TXT record."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#Usage: fulldomain txtvalue
|
||||
dns_eurodns_rm() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
_info "Using EuroDNS DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
|
||||
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
|
||||
|
||||
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
|
||||
EURODNS_APP_ID=""
|
||||
EURODNS_API_KEY=""
|
||||
_err "You didn't specify EuroDNS App ID and API Key."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_info "Removing TXT record"
|
||||
if _eurodns_rm_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
|
||||
_info "Removed TXT record successfully."
|
||||
return 0
|
||||
else
|
||||
_err "Failed to remove TXT record."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_eurodns_rest GET "dns-zones/$h"
|
||||
if [ "$?" != "0" ]; then
|
||||
if [ "$_code" = "404" ]; then
|
||||
_debug "Zone $h not found, continuing..."
|
||||
else
|
||||
_err "API error looking up zone $h"
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
continue
|
||||
fi
|
||||
|
||||
if _contains "$response" '"name"'; then
|
||||
if [ "$i" = "1" ]; then
|
||||
_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
|
||||
}
|
||||
|
||||
_eurodns_add_txt_record() {
|
||||
domain=$1
|
||||
subdomain=$2
|
||||
txtvalue=$3
|
||||
|
||||
data='[{"type":"TXT","host":"'"$subdomain"'","rdata":"'"$txtvalue"'","ttl":'"$EURODNS_TTL"'}]'
|
||||
|
||||
_debug "Adding TXT record via API"
|
||||
if _eurodns_rest POST "dns-zones/$domain/dns-records" "$data"; then
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
_err "Failed to add TXT record"
|
||||
return 1
|
||||
}
|
||||
|
||||
_eurodns_rm_txt_record() {
|
||||
domain=$1
|
||||
subdomain=$2
|
||||
txtvalue=$3
|
||||
|
||||
_debug "Getting current zone data for $domain"
|
||||
|
||||
if ! _eurodns_rest GET "dns-zones/$domain"; then
|
||||
_err "Failed to get zone data"
|
||||
return 1
|
||||
fi
|
||||
|
||||
zone_data=$(echo "$response" | _normalizeJson)
|
||||
_debug2 zone_data "$zone_data"
|
||||
|
||||
# Find the record ID matching our TXT record
|
||||
record_id=$(echo "$zone_data" | tr '{' '\n' | grep -F '"TXT"' | grep -F "\"$subdomain\"" | grep -F "\"$txtvalue\"" | _egrep_o '"id" *: *[0-9]+' | cut -d : -f 2 | _head_n 1)
|
||||
_debug record_id "$record_id"
|
||||
|
||||
if [ -z "$record_id" ]; then
|
||||
_info "TXT record not found or already removed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "Deleting TXT record $record_id"
|
||||
if ! _eurodns_rest DELETE "dns-zones/$domain/dns-records/$record_id"; then
|
||||
_err "Failed to delete TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: _eurodns_rest METHOD ENDPOINT [DATA]
|
||||
_eurodns_rest() {
|
||||
method=$1
|
||||
endpoint=$2
|
||||
data="$3"
|
||||
|
||||
export _H1="X-APP-ID: $EURODNS_APP_ID"
|
||||
export _H2="X-API-KEY: $EURODNS_API_KEY"
|
||||
export _H3="Content-Type: application/json"
|
||||
|
||||
url="$EURODNS_API_URL/$endpoint"
|
||||
|
||||
_debug2 url "$url"
|
||||
_debug2 method "$method"
|
||||
_debug2 data "$data"
|
||||
|
||||
: >"$HTTP_HEADER"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
response="$(_get "$url")"
|
||||
else
|
||||
response="$(_post "$data" "$url" "" "$method")"
|
||||
fi
|
||||
|
||||
_ret="$?"
|
||||
unset _H1 _H2 _H3
|
||||
_debug2 response "$response"
|
||||
|
||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
||||
_debug2 _code "$_code"
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Error calling API: $endpoint"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_code" != "200" ] && [ "$_code" != "201" ] && [ "$_code" != "204" ]; then
|
||||
if [ "$_code" != "404" ]; then
|
||||
_err "API error (HTTP $_code): $response"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
Executable → Regular
+132
-94
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_firestorm_info='Firestorm.ch
|
||||
Site: firestorm.ch
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_firestorm
|
||||
Options:
|
||||
FST_Key Customer ID
|
||||
FST_Secret API Secret
|
||||
FST_Url API URL. Optional. Default "https://api.firestorm.ch/acme-dns".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6839
|
||||
Author: FireStorm GmbH
|
||||
'
|
||||
|
||||
FST_Url_DEFAULT="https://api.firestorm.ch/acme-dns"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: dns_firestorm_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_firestorm_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
|
||||
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
|
||||
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
|
||||
|
||||
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
|
||||
_err "FST_Key and FST_Secret must be set"
|
||||
_err "Get your API credentials at https://admin.firestorm.ch"
|
||||
return 1
|
||||
fi
|
||||
|
||||
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
|
||||
|
||||
_saveaccountconf_mutable FST_Key "$FST_Key"
|
||||
_saveaccountconf_mutable FST_Secret "$FST_Secret"
|
||||
if [ "$FST_Url" != "$FST_Url_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable FST_Url "$FST_Url"
|
||||
else
|
||||
_clearaccountconf_mutable FST_Url
|
||||
fi
|
||||
|
||||
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
|
||||
|
||||
_info "Adding TXT record for $fulldomain"
|
||||
_debug "Subdomain" "$subdomain"
|
||||
_debug "TXT value" "$txtvalue"
|
||||
|
||||
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
|
||||
|
||||
response="$(_firestorm_api "update" "$body")"
|
||||
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
_info "TXT record added successfully"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to add TXT record: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: dns_firestorm_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_firestorm_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
|
||||
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
|
||||
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
|
||||
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
|
||||
|
||||
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
|
||||
_err "FST_Key and FST_Secret must be set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
|
||||
|
||||
_info "Removing TXT record for $fulldomain"
|
||||
|
||||
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
|
||||
|
||||
response="$(_firestorm_api "remove" "$body")"
|
||||
|
||||
if _contains "$response" "removed"; then
|
||||
_info "TXT record removed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to remove TXT record: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# Escape special characters for safe JSON string interpolation
|
||||
_json_safe() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
_firestorm_api() {
|
||||
action=$1
|
||||
data=$2
|
||||
|
||||
export _H1="X-Api-User: $FST_Key"
|
||||
export _H2="X-Api-Key: $FST_Secret"
|
||||
export _H3="Content-Type: application/json"
|
||||
|
||||
_post "$data" "$FST_Url/$action" "" "POST"
|
||||
}
|
||||
@@ -305,7 +305,7 @@ _freedns_domain_id() {
|
||||
fi
|
||||
|
||||
domain_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's/<tr>/@<tr>/g' | tr '@' '\n' |
|
||||
grep "<td>$search_domain</td>\|<td>$search_domain(.*)</td>" |
|
||||
grep -E "<td>$search_domain</td>|<td>$search_domain\(.*\)</td>" |
|
||||
sed -n 's/.*\(edit\.php?edit_domain_id=[0-9a-zA-Z]*\).*/\1/p' |
|
||||
cut -d = -f 2)"
|
||||
# The above beauty extracts domain ID from the html page...
|
||||
|
||||
@@ -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"
|
||||
|
||||
+38
-9
@@ -69,7 +69,12 @@ dns_gd_add() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _contains "$response" "$txtvalue"; then
|
||||
if _contains "$response" "UNKNOWN_DOMAIN"; then
|
||||
# GoDaddy sometimes returns UNKNOWN_DOMAIN when reading a record back even
|
||||
# though the PUT above succeeded; skip the local readback check and let
|
||||
# acme.sh's own DNS propagation check verify the record was published.
|
||||
_info "GoDaddy API won't allow reading the record back; skipping local verification."
|
||||
elif ! _contains "$response" "$txtvalue"; then
|
||||
_err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!"
|
||||
return 1
|
||||
fi
|
||||
@@ -145,8 +150,8 @@ dns_gd_rm() {
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=2
|
||||
p=1
|
||||
i=1
|
||||
p=0
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
@@ -154,17 +159,41 @@ _get_root() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _gd_rest GET "domains/$h"; then
|
||||
return 1
|
||||
# The record name is whatever precedes the candidate zone. Do not assume
|
||||
# _acme-challenge here: with DNS alias mode it can be any name, and the
|
||||
# record may even sit at the zone apex (name "@").
|
||||
if [ "$p" = "0" ]; then
|
||||
_probe_sub="@"
|
||||
else
|
||||
_probe_sub=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
fi
|
||||
|
||||
if _contains "$response" '"code":"NOT_FOUND"'; then
|
||||
_debug "$h not found"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
# Probe with the records endpoint instead of "GET domains/$h": since
|
||||
# 2024-05 GoDaddy rejects the domain details call for accounts with
|
||||
# fewer than 10 domains, while record-level calls keep working.
|
||||
# https://github.com/acmesh-official/acme.sh/issues/4487
|
||||
if ! _gd_rest GET "domains/$h/records/TXT/$_probe_sub"; then
|
||||
return 1
|
||||
fi
|
||||
if _startswith "$response" '\['; then
|
||||
_sub_domain="$_probe_sub"
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Some accounts get UNKNOWN_DOMAIN when reading records of a valid zone
|
||||
# even though writes succeed (see issue #6517); fall back to the domain
|
||||
# details call for them.
|
||||
if ! _gd_rest GET "domains/$h"; then
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" '"domainId"'; then
|
||||
_sub_domain="$_probe_sub"
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "$h not found"
|
||||
p="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_glesys_info='Glesys
|
||||
Site: Glesys.se
|
||||
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_glesys
|
||||
Options:
|
||||
GLESYS_API_KEY Generated API key.
|
||||
GLESYS_PROJECT_ID Project ID for the API key (e.g. cl12345).
|
||||
GLESYS_API API endpoint. Default "https://api.glesys.com/domain".
|
||||
GLESYS_TTL TXT record TTL. Default 120.
|
||||
Issues: https://github.com/acmesh-official/acme.sh/issues/7057
|
||||
Author: Toni Karppi
|
||||
'
|
||||
|
||||
GLESYS_API_DEFAULT="https://api.glesys.com/domain"
|
||||
GLESYS_TTL_DEFAULT="120"
|
||||
|
||||
######## Public functions #####################################################
|
||||
|
||||
# Usage:
|
||||
# dns_glesys_add _acme-challenge.www.example.com "txt-value"
|
||||
dns_glesys_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
_glesys_init || return 1
|
||||
|
||||
if ! _glesys_get_root "$fulldomain"; then
|
||||
_err "Could not find root zone for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
host_value="${_sub_domain:-@}"
|
||||
_debug _host_value "$host_value"
|
||||
|
||||
data="{\"domainname\":\"$_domain\",\"host\":\"$host_value\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":\"$GLESYS_TTL\"}"
|
||||
|
||||
_debug2 data "$data"
|
||||
|
||||
if ! _glesys_rest POST "/addrecord" "$data"; then
|
||||
_err "Failed to send HTTP request to add TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
response_code=$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
_egrep_o '"code":"?[0-9]+' |
|
||||
_egrep_o '[0-9]+$'
|
||||
)
|
||||
|
||||
_debug response_code "$response_code"
|
||||
|
||||
if [ "$response_code" != "200" ]; then
|
||||
_err "GleSYS API responded with an unexpected status when attempting to add TXT record"
|
||||
_debug2 "API response" "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "TXT record added"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage:
|
||||
# dns_glesys_rm _acme-challenge.www.example.com "txt-value"
|
||||
dns_glesys_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
_glesys_init || return 1
|
||||
|
||||
if ! _glesys_get_root "$fulldomain"; then
|
||||
_err "Could not find root zone for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _glesys_find_record_id "$txtvalue"; then
|
||||
_info "TXT record not present, skip removal"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug _record_id "$_record_id"
|
||||
|
||||
if ! _glesys_rest POST "/deleterecord" "{\"recordid\":$_record_id}"; then
|
||||
_err "Failed to send HTTP request to remove TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
response_code=$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
_egrep_o '"code":"?[0-9]+' |
|
||||
_egrep_o '[0-9]+$'
|
||||
)
|
||||
|
||||
_debug response_code "$response_code"
|
||||
|
||||
if [ "$response_code" != "200" ]; then
|
||||
_err "GleSYS API responded with unexpected status when attempting to remove TXT record"
|
||||
_debug2 "API response" "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "TXT record removed"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
######## Private functions ####################################################
|
||||
|
||||
_glesys_find_record_id() {
|
||||
txtvalue="$1"
|
||||
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
if [ -z "$txtvalue" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_record_id=""
|
||||
|
||||
_debug "Looking for TXT record with value" "$txtvalue"
|
||||
|
||||
if ! _glesys_rest GET "/listrecords?domainname=$_domain"; then
|
||||
_err "Failed to list DNS records"
|
||||
return 1
|
||||
fi
|
||||
|
||||
records="$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
sed 's/},{/}\
|
||||
{/g'
|
||||
)"
|
||||
|
||||
_debug2 records "$records"
|
||||
|
||||
expected_data="\"data\":\"$txtvalue\""
|
||||
|
||||
_record_id="$(
|
||||
printf "%s\n" "$records" |
|
||||
while IFS= read -r record; do
|
||||
printf "%s" "$record" | grep -q '"type":"TXT"' || continue
|
||||
printf "%s" "$record" | grep -Fq "$expected_data" || continue
|
||||
|
||||
printf "%s" "$record" |
|
||||
grep -E -o '"recordid":"?[0-9]+' |
|
||||
grep -E -o '[0-9]+$'
|
||||
|
||||
break
|
||||
done
|
||||
)"
|
||||
|
||||
_debug _record_id "$_record_id"
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Finds:
|
||||
# _domain example.com
|
||||
# _sub_domain _acme-challenge.www
|
||||
_glesys_get_root() {
|
||||
domain="$1"
|
||||
i=1
|
||||
|
||||
while true; do
|
||||
h="$(printf "%s" "$domain" | cut -d . -f "$i"-100)"
|
||||
|
||||
if [ -z "$h" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _glesys_rest GET "/listrecords?domainname=$h"; then
|
||||
response_code=$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
_egrep_o '"code":"?[0-9]+' |
|
||||
_egrep_o '[0-9]+$'
|
||||
)
|
||||
|
||||
_debug response_code "$response_code"
|
||||
|
||||
if [ "$response_code" = "200" ]; then
|
||||
cut_len="$((${#domain} - ${#h} - 1))"
|
||||
_domain="$h"
|
||||
_sub_domain="$(printf "%s" "$domain" | cut -c "1-$cut_len")"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
i="$((i + 1))"
|
||||
done
|
||||
}
|
||||
|
||||
_glesys_init() {
|
||||
[ -z "$GLESYS_API" ] && GLESYS_API="$GLESYS_API_DEFAULT"
|
||||
[ -z "$GLESYS_TTL" ] && GLESYS_TTL="$GLESYS_TTL_DEFAULT"
|
||||
|
||||
_debug GLESYS_API "$GLESYS_API"
|
||||
_debug GLESYS_TTL "$GLESYS_TTL"
|
||||
|
||||
GLESYS_API_KEY="${GLESYS_API_KEY:-$(_readaccountconf_mutable GLESYS_API_KEY)}"
|
||||
GLESYS_PROJECT_ID="${GLESYS_PROJECT_ID:-$(_readaccountconf_mutable GLESYS_PROJECT_ID)}"
|
||||
|
||||
if [ -z "$GLESYS_API_KEY" ] || [ -z "$GLESYS_PROJECT_ID" ]; then
|
||||
_err "GLESYS_API_KEY and GLESYS_PROJECT_ID must be set for this provider"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_secure_debug GLESYS_API_KEY "$GLESYS_API_KEY"
|
||||
_secure_debug GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID"
|
||||
|
||||
_glesys_basic_auth="$(printf "%s:%s" "$GLESYS_PROJECT_ID" "$GLESYS_API_KEY" | _base64)"
|
||||
_secure_debug2 _glesys_basic_auth "$_glesys_basic_auth"
|
||||
|
||||
_saveaccountconf_mutable GLESYS_API_KEY "$GLESYS_API_KEY"
|
||||
_saveaccountconf_mutable GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_glesys_rest() {
|
||||
method="$1"
|
||||
path="$2"
|
||||
data="$3"
|
||||
|
||||
export _H1="Authorization: Basic $_glesys_basic_auth"
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Accept: application/json"
|
||||
|
||||
url="$GLESYS_API$path"
|
||||
_debug "$method $url"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
response="$(_get "$url")"
|
||||
else
|
||||
response="$(_post "$data" "$url" "" "$method")"
|
||||
fi
|
||||
|
||||
ret="$?"
|
||||
_debug2 response "$response"
|
||||
_debug ret "$ret"
|
||||
|
||||
if [ "$ret" != "0" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_hetzner_info='Hetzner.com
|
||||
Site: Hetzner.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_hetzner
|
||||
Options:
|
||||
HETZNER_Token API Token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2943
|
||||
'
|
||||
|
||||
HETZNER_Api="https://dns.hetzner.com/api/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
# Used to add txt record
|
||||
# Ref: https://dns.hetzner.com/api-docs/
|
||||
dns_hetzner_add() {
|
||||
full_domain=$1
|
||||
txt_value=$2
|
||||
|
||||
HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
|
||||
|
||||
if [ -z "$HETZNER_Token" ]; then
|
||||
HETZNER_Token=""
|
||||
_err "You didn't specify a Hetzner api token."
|
||||
_err "You can get yours from here https://dns.hetzner.com/settings/api-token."
|
||||
return 1
|
||||
fi
|
||||
|
||||
#save the api key and email to the account conf file.
|
||||
_saveaccountconf_mutable HETZNER_Token "$HETZNER_Token"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
|
||||
if ! _get_root "$full_domain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting TXT records"
|
||||
if ! _find_record "$_sub_domain" "$txt_value"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "Adding record"
|
||||
if _hetzner_rest POST "records" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
|
||||
if _contains "$response" "$txt_value"; then
|
||||
_info "Record added, OK"
|
||||
_sleep 2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error${_response_error}"
|
||||
return 1
|
||||
else
|
||||
_info "Found record id: $_record_id."
|
||||
_info "Record found, do nothing."
|
||||
return 0
|
||||
# we could modify a record, if the names for txt records for *.example.com and example.com would be not the same
|
||||
#if _hetzner_rest PUT "records/${_record_id}" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$full_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
|
||||
# if _contains "$response" "$txt_value"; then
|
||||
# _info "Modified, OK"
|
||||
# return 0
|
||||
# fi
|
||||
#fi
|
||||
#_err "Add txt record error (modify)."
|
||||
#return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage: full_domain txt_value
|
||||
# Used to remove the txt record after validation
|
||||
dns_hetzner_rm() {
|
||||
full_domain=$1
|
||||
txt_value=$2
|
||||
|
||||
HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$full_domain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting TXT records"
|
||||
if ! _find_record "$_sub_domain" "$txt_value"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "Remove not needed. Record not found."
|
||||
else
|
||||
if ! _hetzner_rest DELETE "records/$_record_id"; then
|
||||
_err "Delete record error${_response_error}"
|
||||
return 1
|
||||
fi
|
||||
_sleep 2
|
||||
_info "Record deleted"
|
||||
fi
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#returns
|
||||
# _record_id=a8d58f22d6931bf830eaa0ec6464bf81 if found; or 1 if error
|
||||
_find_record() {
|
||||
unset _record_id
|
||||
_record_name=$1
|
||||
_record_value=$2
|
||||
|
||||
if [ -z "$_record_value" ]; then
|
||||
_record_value='[^"]*'
|
||||
fi
|
||||
|
||||
_debug "Getting all records"
|
||||
_hetzner_rest GET "records?zone_id=${_domain_id}"
|
||||
|
||||
if _response_has_error; then
|
||||
_err "Error${_response_error}"
|
||||
return 1
|
||||
else
|
||||
_record_id=$(
|
||||
echo "$response" |
|
||||
grep -o "{[^\{\}]*\"name\":\"$_record_name\"[^\}]*}" |
|
||||
grep "\"value\":\"$_record_value\"" |
|
||||
while read -r record; do
|
||||
# test for type and
|
||||
if [ -n "$(echo "$record" | _egrep_o '"type":"TXT"')" ]; then
|
||||
echo "$record" | _egrep_o '"id":"[^"]*"' | cut -d : -f 2 | tr -d \"
|
||||
break
|
||||
fi
|
||||
done
|
||||
)
|
||||
fi
|
||||
}
|
||||
|
||||
#_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
|
||||
|
||||
domain_without_acme=$(echo "$domain" | cut -d . -f 2-)
|
||||
domain_param_name=$(echo "HETZNER_Zone_ID_for_${domain_without_acme}" | sed 's/[\.\-]/_/g')
|
||||
|
||||
_debug "Reading zone_id for '$domain_without_acme' from config..."
|
||||
HETZNER_Zone_ID=$(_readdomainconf "$domain_param_name")
|
||||
if [ "$HETZNER_Zone_ID" ]; then
|
||||
_debug "Found, using: $HETZNER_Zone_ID"
|
||||
if ! _hetzner_rest GET "zones/${HETZNER_Zone_ID}"; then
|
||||
_debug "Zone with id '$HETZNER_Zone_ID' does not exist."
|
||||
_cleardomainconf "$domain_param_name"
|
||||
unset HETZNER_Zone_ID
|
||||
else
|
||||
if _contains "$response" "\"id\":\"$HETZNER_Zone_ID\""; then
|
||||
_domain=$(printf "%s\n" "$response" | _egrep_o '"name":"[^"]*"' | cut -d : -f 2 | tr -d \" | head -n 1)
|
||||
if [ "$_domain" ]; then
|
||||
_cut_length=$((${#domain} - ${#_domain} - 1))
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cut_length")
|
||||
_domain_id="$HETZNER_Zone_ID"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
_debug "Trying to get zone id by domain name for '$domain_without_acme'."
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
return 1
|
||||
fi
|
||||
_debug h "$h"
|
||||
|
||||
_hetzner_rest GET "zones?name=$h"
|
||||
|
||||
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then
|
||||
_domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
HETZNER_Zone_ID=$_domain_id
|
||||
_savedomainconf "$domain_param_name" "$HETZNER_Zone_ID"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
#returns
|
||||
# _response_error
|
||||
_response_has_error() {
|
||||
unset _response_error
|
||||
|
||||
err_part="$(echo "$response" | _egrep_o '"error":\{[^\}]*\}')"
|
||||
|
||||
if [ -n "$err_part" ]; then
|
||||
err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2)
|
||||
err_message=$(echo "$err_part" | _egrep_o '"message":"[^"]+"' | cut -d : -f 2 | tr -d \")
|
||||
|
||||
if [ -n "$err_code" ] && [ -n "$err_message" ]; then
|
||||
_response_error=" - message: ${err_message}, code: ${err_code}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
#returns
|
||||
# response
|
||||
_hetzner_rest() {
|
||||
m=$1
|
||||
ep="$2"
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
key_trimmed=$(echo "$HETZNER_Token" | tr -d \")
|
||||
|
||||
export _H1="Content-TType: application/json"
|
||||
export _H2="Auth-API-Token: $key_trimmed"
|
||||
|
||||
if [ "$m" != "GET" ]; then
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$HETZNER_Api/$ep" "" "$m")"
|
||||
else
|
||||
response="$(_get "$HETZNER_Api/$ep")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ] || _response_has_error; then
|
||||
_debug "Error$_response_error"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
@@ -40,6 +40,11 @@ _hostingde_apiKey() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The endpoint is the base URL only; the api path is appended below.
|
||||
# hosting.de's own docs show the full api URL, so strip it if pasted in.
|
||||
# https://github.com/acmesh-official/acme.sh/issues/6896
|
||||
HOSTINGDE_ENDPOINT="$(echo "$HOSTINGDE_ENDPOINT" | sed 's|/api/dns/v1/json||; s|/*$||')"
|
||||
|
||||
_saveaccountconf_mutable HOSTINGDE_APIKEY "$HOSTINGDE_APIKEY"
|
||||
_saveaccountconf_mutable HOSTINGDE_ENDPOINT "$HOSTINGDE_ENDPOINT"
|
||||
}
|
||||
|
||||
Executable
+196
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_hostinger_info='Hostinger
|
||||
Site: Hostinger.com
|
||||
Domains: hostinger.nl
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hostinger
|
||||
Options:
|
||||
HOSTINGER_Token API Key
|
||||
Issues: https://github.com/acmesh-official/acme.sh/issues/6831
|
||||
Author: Sasha Reid <github@sasha.hackl.es>
|
||||
'
|
||||
|
||||
HOSTINGER_Api="https://developers.hostinger.com/api/dns/v1/zones"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_hostinger_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
HOSTINGER_Token="${HOSTINGER_Token:-$(_readaccountconf_mutable HOSTINGER_Token)}"
|
||||
|
||||
if [ -z "$HOSTINGER_Token" ]; then
|
||||
HOSTINGER_Token=""
|
||||
_err "You didn't specify a Hostinger API Key yet."
|
||||
_err "Please read the documentation for the Hostinger API authentication at https://developers.hostinger.com/#description/authentication"
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable HOSTINGER_Token "$HOSTINGER_Token"
|
||||
|
||||
_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 existing records"
|
||||
_hostinger_rest GET "${_domain}"
|
||||
|
||||
if [ -z "$response" ]; then
|
||||
_err "Error"
|
||||
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 record"
|
||||
if _hostinger_rest PUT "$_domain" "{\"zone\":[{\"name\": \"$_sub_domain\",\"records\": [{\"content\":\"$txtvalue\"}],\"type\":\"TXT\",\"ttl\":\"120\"}],\"overwrite\":false}"; then
|
||||
if _contains "$response" "Request accepted"; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
elif _contains "$response" "DNS resource record is not valid or conflicts with another resource record" ||
|
||||
_contains "$response" 'DNS:4008'; then
|
||||
_info "Already exists, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
|
||||
}
|
||||
|
||||
#fulldomain txtvalue
|
||||
dns_hostinger_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
HOSTINGER_Token="${HOSTINGER_Token:-$(_readaccountconf_mutable HOSTINGER_Token)}"
|
||||
|
||||
if [ -z "$HOSTINGER_Token" ]; then
|
||||
HOSTINGER_Token=""
|
||||
_err "You didn't specify a Hostinger API Key yet."
|
||||
_err "Please read the documentation for the Hostinger API authentication at https://developers.hostinger.com/#description/authentication"
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable HOSTINGER_Token "$HOSTINGER_Token"
|
||||
|
||||
_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 existing records"
|
||||
_hostinger_rest GET "${_domain}"
|
||||
|
||||
if [ -z "$response" ]; then
|
||||
_err "Error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"name\":\"$_sub_domain\""; then
|
||||
# Match the record, and make certain it is a TXT record for the domain not another type. Then remove our target record from the list
|
||||
remaining_records=$(echo "$response" | _normalizeJson | _egrep_o '{"name":"'"$_sub_domain"'","records":\[[^]]+\],"ttl":[0-9]+,"type":"TXT"\}' | _egrep_o "\[.*\]" | sed -E 's#\{"content":"\\"'"$txtvalue"'\\"","is_disabled":false\},?##g')
|
||||
if [ "$remaining_records" != "[]" ]; then
|
||||
remaining_json=$(echo "$remaining_records" | _egrep_o '"content":"\\"[^}]+\\""' | sed -E 's/^(.*)$/{\1},/g' | tr -d '\n' | sed 's/,$//')
|
||||
# We need to set the remaining records back to Hostinger, as we can't partially delete
|
||||
_info "Removing $txtvalue from $_sub_domain by setting records to ${remaining_json}"
|
||||
if _hostinger_rest PUT "$_domain" "{\"zone\":[{\"name\": \"$_sub_domain\",\"records\": [${remaining_json}],\"type\":\"TXT\",\"ttl\":\"120\"}],\"overwrite\":true}"; then
|
||||
if _contains "$response" "Request accepted"; then
|
||||
_info "Updated remaining records, OK"
|
||||
return 0
|
||||
elif _contains "$response" "DNS resource record is not valid or conflicts with another resource record" ||
|
||||
_contains "$response" 'DNS:4008'; then
|
||||
_info "Already exists, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
# Otherwise delete the TXT record that matches the subdomain
|
||||
else
|
||||
if ! _hostinger_rest DELETE "$_domain" "{\"filters\":[{\"name\":\"$_sub_domain\",\"type\":\"TXT\"}]}"; then
|
||||
_err "Delete record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
echo "$response" | grep "Request accepted" >/dev/null
|
||||
else
|
||||
_info "Don't need to remove."
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
return 1
|
||||
fi
|
||||
|
||||
_hostinger_rest GET "$h"
|
||||
if _contains "$response" "records"; then
|
||||
if [ "$response" = "[]" ]; then
|
||||
_debug "Valid subdomains are not the root"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_hostinger_rest() {
|
||||
m=$1
|
||||
ep="$2"
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
token_trimmed=$(echo "$HOSTINGER_Token" | tr -d '"')
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
export _H2="Authorization: Bearer $token_trimmed"
|
||||
|
||||
if [ "$m" != "GET" ]; then
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$HOSTINGER_Api/$ep" "" "$m")"
|
||||
else
|
||||
response="$(_get "$HOSTINGER_Api/$ep")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "error $ep"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
#!/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/v2).
|
||||
HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds).
|
||||
HOSTUP_ZONE_ID Optional. Force a specific v2 zone ID (zone_...) and skip auto-detection.
|
||||
Author: HostUp (https://cloud.hostup.se/contact/en)
|
||||
'
|
||||
|
||||
HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api/v2"
|
||||
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"
|
||||
hostup_add_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")"
|
||||
hostup_add_record_value="$(_hostup_json_escape "$hostup_add_txtvalue")"
|
||||
|
||||
raw_ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}"
|
||||
ttl="$(_hostup_normalize_ttl "$raw_ttl")"
|
||||
if [ -z "$ttl" ]; then
|
||||
_err "HOSTUP_TTL must be a whole number between 60 and 86400 seconds."
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$HOSTUP_TTL" ]; then
|
||||
HOSTUP_TTL="$ttl"
|
||||
_saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL"
|
||||
fi
|
||||
|
||||
_debug "zone_id" "$HOSTUP_ZONE_ID"
|
||||
_debug "zone_domain" "$HOSTUP_ZONE_DOMAIN"
|
||||
_debug "record_name" "$record_name"
|
||||
_debug "ttl" "$ttl"
|
||||
|
||||
record_name_fqdn="$(_hostup_fqdn "$fulldomain")"
|
||||
if _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$hostup_add_txtvalue"; then
|
||||
_info "TXT record already exists for $fulldomain"
|
||||
return 0
|
||||
fi
|
||||
|
||||
request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$hostup_add_record_value\",\"ttl\":$ttl}"
|
||||
|
||||
if ! _hostup_rest "POST" "/dns-zones/$HOSTUP_ZONE_ID/records" "$request_body"; then
|
||||
return 1
|
||||
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"
|
||||
|
||||
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" "$record_value"
|
||||
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" "$record_value"
|
||||
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
|
||||
HOSTUP_API_BASE="$(_hostup_normalize_api_base "$HOSTUP_API_BASE")"
|
||||
|
||||
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, write:dns, and read:domains scopes."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable HOSTUP_API_KEY "$HOSTUP_API_KEY"
|
||||
_saveaccountconf_mutable HOSTUP_API_BASE "$HOSTUP_API_BASE"
|
||||
|
||||
if [ -n "$HOSTUP_ZONE_ID" ]; then
|
||||
_saveaccountconf_mutable HOSTUP_ZONE_ID "$HOSTUP_ZONE_ID"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_hostup_normalize_api_base() {
|
||||
api_base="${1%/}"
|
||||
|
||||
case "$api_base" in
|
||||
*/api/v2)
|
||||
printf "%s" "$api_base"
|
||||
;;
|
||||
*/api)
|
||||
printf "%s/v2" "$api_base"
|
||||
;;
|
||||
*)
|
||||
printf "%s" "$api_base"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_hostup_normalize_ttl() {
|
||||
ttl_value="$1"
|
||||
|
||||
case "$ttl_value" in
|
||||
"" | *[!0-9]*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
while [ "${ttl_value#0}" != "$ttl_value" ]; do
|
||||
ttl_value="${ttl_value#0}"
|
||||
done
|
||||
[ -z "$ttl_value" ] && ttl_value=0
|
||||
|
||||
case "$ttl_value" in
|
||||
??????*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$ttl_value" -lt 60 ] || [ "$ttl_value" -gt 86400 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "%s" "$ttl_value"
|
||||
}
|
||||
|
||||
_hostup_domain_in_zone() {
|
||||
host="$(printf "%s" "${1%.}" | _lower_case)"
|
||||
zone="$(printf "%s" "${2%.}" | _lower_case)"
|
||||
|
||||
if [ -z "$host" ] || [ -z "$zone" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$host" = "$zone" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$host" in
|
||||
*."$zone")
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_hostup_detect_zone() {
|
||||
fulldomain="$1"
|
||||
|
||||
if [ -n "$HOSTUP_ZONE_ID" ] && [ -n "$HOSTUP_ZONE_DOMAIN" ]; then
|
||||
if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then
|
||||
return 0
|
||||
fi
|
||||
_debug "hostup_cached_zone_mismatch" "$HOSTUP_ZONE_DOMAIN"
|
||||
HOSTUP_ZONE_ID=""
|
||||
HOSTUP_ZONE_DOMAIN=""
|
||||
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
|
||||
if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then
|
||||
return 0
|
||||
fi
|
||||
_debug "hostup_forced_zone_mismatch" "$HOSTUP_ZONE_DOMAIN"
|
||||
fi
|
||||
HOSTUP_ZONE_ID=""
|
||||
HOSTUP_ZONE_DOMAIN=""
|
||||
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="$(_hostup_json_extract "name" "$_hostup_response")"
|
||||
if [ -n "$zonedomain" ]; then
|
||||
HOSTUP_ZONE_DOMAIN="$zonedomain"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_hostup_load_zones() {
|
||||
if ! _hostup_rest "GET" "/dns-zones?limit=1000" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
HOSTUP_ZONES_CACHE=""
|
||||
data="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
*'"id"'*'"name"'*)
|
||||
zone_id="$(_hostup_json_extract "id" "$line")"
|
||||
zone_domain="$(_hostup_json_extract "name" "$line")"
|
||||
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=""
|
||||
|
||||
encoded_domain="$(printf "%s" "$lookup_domain" | _url_encode)"
|
||||
if _hostup_rest "GET" "/dns-zones?name=$encoded_domain&limit=1" ""; then
|
||||
zone_id="$(_hostup_json_extract "id" "$_hostup_response")"
|
||||
zone_domain="$(_hostup_json_extract "name" "$_hostup_response")"
|
||||
if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then
|
||||
zone_domain_lower="$(printf "%s" "$zone_domain" | _lower_case)"
|
||||
if [ "$zone_domain_lower" = "$lookup_domain" ]; then
|
||||
_lookup_zone_domain="$zone_domain"
|
||||
_lookup_zone_id="$zone_id"
|
||||
HOSTUP_ZONE_DOMAIN="$zone_domain"
|
||||
HOSTUP_ZONE_ID="$zone_id"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$HOSTUP_ZONES_CACHE" ] && ! _hostup_load_zones; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS='|' read -r domain zone_id; do
|
||||
[ -z "$domain" ] && continue
|
||||
domain_lower="$(printf "%s" "$domain" | _lower_case)"
|
||||
if [ "$domain_lower" = "$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() {
|
||||
_hostup_find_zone_id="$1"
|
||||
_hostup_find_fqdn="$2"
|
||||
_hostup_find_txtvalue="$3"
|
||||
|
||||
_hostup_find_encoded_name="$(printf "%s" "$_hostup_find_fqdn" | _url_encode)"
|
||||
if ! _hostup_rest "GET" "/dns-zones/$_hostup_find_zone_id/records?type=TXT&name=$_hostup_find_encoded_name" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
HOSTUP_RECORD_ID=""
|
||||
_hostup_find_records="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
|
||||
|
||||
while IFS= read -r _hostup_find_line; do
|
||||
# Normalize line to make TXT value matching reliable
|
||||
_hostup_find_line_clean="$(printf "%s" "$_hostup_find_line" | tr -d '\r\n')"
|
||||
_hostup_find_line_value_clean="$(printf "%s" "$_hostup_find_line_clean" | sed 's/\\"//g')"
|
||||
|
||||
_hostup_find_record_type="$(_hostup_json_extract "type" "$_hostup_find_line_clean")"
|
||||
[ "$_hostup_find_record_type" != "TXT" ] && continue
|
||||
|
||||
_hostup_find_name_value="$(_hostup_json_extract "name" "$_hostup_find_line_clean")"
|
||||
_hostup_find_record_value="$(_hostup_json_extract "value" "$_hostup_find_line_value_clean")"
|
||||
|
||||
_debug "hostup_record_raw" "$_hostup_find_record_value"
|
||||
if [ "${_hostup_find_record_value#\"}" != "$_hostup_find_record_value" ] && [ "${_hostup_find_record_value%\"}" != "$_hostup_find_record_value" ]; then
|
||||
_hostup_find_record_value="${_hostup_find_record_value#\"}"
|
||||
_hostup_find_record_value="${_hostup_find_record_value%\"}"
|
||||
fi
|
||||
if [ "${_hostup_find_record_value#\'}" != "$_hostup_find_record_value" ] && [ "${_hostup_find_record_value%\'}" != "$_hostup_find_record_value" ]; then
|
||||
_hostup_find_record_value="${_hostup_find_record_value#\'}"
|
||||
_hostup_find_record_value="${_hostup_find_record_value%\'}"
|
||||
fi
|
||||
_hostup_find_record_value="$(printf "%s" "$_hostup_find_record_value" | tr -d '\r\n')"
|
||||
_debug "hostup_record_value" "$_hostup_find_record_value"
|
||||
|
||||
if [ "$_hostup_find_name_value" = "$_hostup_find_fqdn" ] && [ "$_hostup_find_record_value" = "$_hostup_find_txtvalue" ]; then
|
||||
_hostup_find_record_id="$(_hostup_json_extract "id" "$_hostup_find_line_clean")"
|
||||
if [ -n "$_hostup_find_record_id" ]; then
|
||||
HOSTUP_RECORD_ID="$_hostup_find_record_id"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done <<EOF
|
||||
$_hostup_find_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\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | _head_n 1)"
|
||||
if [ -n "$quoted_match" ]; then
|
||||
printf "%s" "$quoted_match" |
|
||||
cut -d : -f2- |
|
||||
sed 's/^[[:space:]]*"//' |
|
||||
sed 's/"[[:space:]]*$//' |
|
||||
sed 's/\\"/"/g'
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fallback for unquoted values (e.g., numeric IDs)
|
||||
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*[^,}]*" | _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"
|
||||
txtvalue="$3"
|
||||
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')"
|
||||
if [ -n "$txtvalue" ]; then
|
||||
safe_value="$(printf "%s" "$txtvalue" | sed 's/[^A-Za-z0-9]/_/g')"
|
||||
printf "%s_%s_%s" "$safe_zone" "$safe_domain" "$safe_value"
|
||||
return 0
|
||||
fi
|
||||
printf "%s_%s" "$safe_zone" "$safe_domain"
|
||||
}
|
||||
|
||||
_hostup_clear_record_id() {
|
||||
zone_id="$1"
|
||||
domain="$2"
|
||||
txtvalue="$3"
|
||||
key="$(_hostup_record_key "$zone_id" "$domain" "$txtvalue")"
|
||||
_clearaccountconf_mutable "HOSTUP_RECORD_$key"
|
||||
legacy_key="$(_hostup_record_key "$zone_id" "$domain")"
|
||||
if [ "$legacy_key" != "$key" ]; then
|
||||
_clearaccountconf_mutable "HOSTUP_RECORD_$legacy_key"
|
||||
fi
|
||||
}
|
||||
|
||||
_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
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_hostup_problem_error() {
|
||||
problem_code="$(_hostup_json_extract "code" "$_hostup_response")"
|
||||
problem_detail="$(_hostup_json_extract "detail" "$_hostup_response")"
|
||||
|
||||
if [ -n "$problem_detail" ]; then
|
||||
if [ -n "$problem_code" ]; then
|
||||
_err "HostUp API error ($problem_code): $problem_detail"
|
||||
else
|
||||
_err "HostUp API error: $problem_detail"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_hostup_rest() {
|
||||
method="$1"
|
||||
route="$2"
|
||||
data="$3"
|
||||
|
||||
_hostup_response=""
|
||||
|
||||
export _H1="Authorization: Bearer $HOSTUP_API_KEY"
|
||||
export _H2="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
|
||||
|
||||
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)
|
||||
_hostup_problem_error || _err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions."
|
||||
return 1
|
||||
;;
|
||||
403)
|
||||
_hostup_problem_error || _err "HostUp API returned 403 Forbidden. The API key lacks required DNS/domain scopes."
|
||||
return 1
|
||||
;;
|
||||
404)
|
||||
_hostup_problem_error || _err "HostUp API returned 404 Not Found for $route"
|
||||
return 1
|
||||
;;
|
||||
429)
|
||||
_hostup_problem_error || _err "HostUp API rate limit exceeded. Please retry later."
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
_hostup_problem_error || _err "HostUp API request failed with status $http_status"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -7,11 +7,11 @@ Options:
|
||||
HUAWEICLOUD_Username Username
|
||||
HUAWEICLOUD_Password Password
|
||||
HUAWEICLOUD_DomainName DomainName
|
||||
HUAWEICLOUD_Region Region. E.g. "cn-north-4". Optional, defaults to "ap-southeast-1".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/3265
|
||||
'
|
||||
|
||||
iam_api="https://iam.myhuaweicloud.com"
|
||||
dns_api="https://dns.ap-southeast-1.myhuaweicloud.com" # Should work
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
@@ -30,6 +30,7 @@ dns_huaweicloud_add() {
|
||||
HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}"
|
||||
HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}"
|
||||
HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}"
|
||||
HUAWEICLOUD_Region="${HUAWEICLOUD_Region:-$(_readaccountconf_mutable HUAWEICLOUD_Region)}"
|
||||
|
||||
# Check information
|
||||
if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then
|
||||
@@ -37,8 +38,11 @@ dns_huaweicloud_add() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_huaweicloud_region="${HUAWEICLOUD_Region:-ap-southeast-1}"
|
||||
dns_api="https://dns.${_huaweicloud_region}.myhuaweicloud.com"
|
||||
|
||||
unset token # Clear token
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")"
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}" "${_huaweicloud_region}")"
|
||||
if [ -z "${token}" ]; then # Check token
|
||||
_err "dns_api(dns_huaweicloud): Error getting token."
|
||||
return 1
|
||||
@@ -65,6 +69,9 @@ dns_huaweicloud_add() {
|
||||
_saveaccountconf_mutable HUAWEICLOUD_Username "${HUAWEICLOUD_Username}"
|
||||
_saveaccountconf_mutable HUAWEICLOUD_Password "${HUAWEICLOUD_Password}"
|
||||
_saveaccountconf_mutable HUAWEICLOUD_DomainName "${HUAWEICLOUD_DomainName}"
|
||||
if [ -n "${HUAWEICLOUD_Region}" ]; then
|
||||
_saveaccountconf_mutable HUAWEICLOUD_Region "${HUAWEICLOUD_Region}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -81,6 +88,7 @@ dns_huaweicloud_rm() {
|
||||
HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}"
|
||||
HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}"
|
||||
HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}"
|
||||
HUAWEICLOUD_Region="${HUAWEICLOUD_Region:-$(_readaccountconf_mutable HUAWEICLOUD_Region)}"
|
||||
|
||||
# Check information
|
||||
if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then
|
||||
@@ -88,8 +96,11 @@ dns_huaweicloud_rm() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_huaweicloud_region="${HUAWEICLOUD_Region:-ap-southeast-1}"
|
||||
dns_api="https://dns.${_huaweicloud_region}.myhuaweicloud.com"
|
||||
|
||||
unset token # Clear token
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")"
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}" "${_huaweicloud_region}")"
|
||||
if [ -z "${token}" ]; then # Check token
|
||||
_err "dns_api(dns_huaweicloud): Error getting token."
|
||||
return 1
|
||||
@@ -298,6 +309,7 @@ _get_token() {
|
||||
_username=$1
|
||||
_password=$2
|
||||
_domain_name=$3
|
||||
_region_name=$4
|
||||
|
||||
_debug "Getting Token"
|
||||
body="{
|
||||
@@ -318,7 +330,7 @@ _get_token() {
|
||||
},
|
||||
\"scope\": {
|
||||
\"project\": {
|
||||
\"name\": \"ap-southeast-1\"
|
||||
\"name\": \"${_region_name}\"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+49
-57
@@ -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,30 @@ 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
|
||||
response=$(_post "$data" "${INFOMANIAK_API_URL}/2/zones/${zone}/records")
|
||||
if _contains "$response" '"result":"success"'; then
|
||||
_info "Record added"
|
||||
_debug "Response: $response"
|
||||
_debug "response: $response"
|
||||
return 0
|
||||
fi
|
||||
_err "could not create record"
|
||||
_err "Could not create record."
|
||||
_debug "Response: $response"
|
||||
return 1
|
||||
}
|
||||
@@ -106,7 +105,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
|
||||
|
||||
@@ -130,7 +129,7 @@ dns_infomaniak_rm() {
|
||||
fi
|
||||
|
||||
export _H1="Authorization: Bearer $INFOMANIAK_API_TOKEN"
|
||||
export _H2="ContentType: application/json"
|
||||
export _H2="Content-Type: application/json"
|
||||
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
@@ -138,63 +137,56 @@ 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"
|
||||
|
||||
if [ -z "$record_id" ]; then
|
||||
_err "could not find record to delete"
|
||||
_debug "response: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 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
|
||||
response=$(_post "" "${INFOMANIAK_API_URL}/2/zones/${zone}/records/${record_id}" "" DELETE)
|
||||
if _contains "$response" '"result":"success"'; then
|
||||
_info "Record deleted"
|
||||
_debug "response: $response"
|
||||
return 0
|
||||
fi
|
||||
_err "could not delete record"
|
||||
_err "Could not delete record."
|
||||
_debug "Response: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
_get_domain_id() {
|
||||
_get_zone() {
|
||||
domain="$1"
|
||||
|
||||
# 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
|
||||
# Whatever the domain is, you can get the fqdn with the following.
|
||||
response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones")
|
||||
_debug2 "_get_zone response" "$response"
|
||||
if ! _contains "$response" '"result":"success"'; then
|
||||
_err "cannot get zones for ${domain}, response: ${response}"
|
||||
return 1
|
||||
fi
|
||||
echo "$response" | _egrep_o '"fqdn" *: *"[^"]*"' | _head_n 1 | cut -d '"' -f 4
|
||||
}
|
||||
|
||||
+33
-5
@@ -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
|
||||
@@ -300,11 +307,32 @@ _get_root() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "$h"; then
|
||||
# Anchor the match to the XML tag and escape dots so $h is compared
|
||||
# literally: _contains uses grep, which treats "$h" as a regex, and a
|
||||
# bare "g.berlight.de" would match "<string>berlight.de" (the 'g' from
|
||||
# "<string>" plus '.' matching '>'). See issue #5129.
|
||||
_hregex=$(printf "%s" "$h" | sed 's/\./\\./g')
|
||||
if _contains "$response" "<string>$_hregex</string>"; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
# IDN fallback: INWX returns Unicode zone names; when $h is ACE/punycode,
|
||||
# encode each zone name via _idn() and compare -- no python dependency.
|
||||
if _contains "$h" "xn--"; then
|
||||
_zone_unicode=$(printf "%s" "$response" | _egrep_o '<string>[^<]*' |
|
||||
sed 's/<[^>]*>//g' | while IFS= read -r _z; do
|
||||
if [ "$(_idn "$_z")" = "$h" ]; then
|
||||
printf "%s" "$_z"
|
||||
break
|
||||
fi
|
||||
done)
|
||||
if [ -n "$_zone_unicode" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain="$_zone_unicode"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
@@ -324,7 +352,7 @@ _inwx_delete_record() {
|
||||
<member>
|
||||
<name>id</name>
|
||||
<value>
|
||||
<int>%s</int>
|
||||
<string>%s</string>
|
||||
</value>
|
||||
</member>
|
||||
</struct>
|
||||
@@ -362,7 +390,7 @@ _inwx_update_record() {
|
||||
<member>
|
||||
<name>id</name>
|
||||
<value>
|
||||
<int>%s</int>
|
||||
<string>%s</string>
|
||||
</value>
|
||||
</member>
|
||||
</struct>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user