mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-08-13 12:33:30 +02:00
Compare commits
No commits in common. "master" and "3.0.8" have entirely different histories.
264 changed files with 2524 additions and 23039 deletions
190
.github/copilot-instructions.md
vendored
190
.github/copilot-instructions.md
vendored
|
|
@ -1,190 +0,0 @@
|
|||
# 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.
|
||||
50
.github/workflows/Apache.yml
vendored
50
.github/workflows/Apache.yml
vendored
|
|
@ -1,50 +0,0 @@
|
|||
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
|
||||
514
.github/workflows/DNS.yml
vendored
514
.github/workflows/DNS.yml
vendored
|
|
@ -26,9 +26,9 @@ jobs:
|
|||
id: step_one
|
||||
run: |
|
||||
if [ "${{secrets.TokenName1}}" ] ; then
|
||||
echo "hasToken=true" >> "$GITHUB_OUTPUT"
|
||||
echo "::set-output name=hasToken::true"
|
||||
else
|
||||
echo "hasToken=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::set-output name=hasToken::false"
|
||||
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@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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,11 +114,9 @@ jobs:
|
|||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install tools
|
||||
run: |
|
||||
brew untap aws/tap || true
|
||||
brew install socat
|
||||
run: 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
|
||||
|
|
@ -167,7 +165,7 @@ jobs:
|
|||
- name: Set git to use LF
|
||||
run: |
|
||||
git config --global core.autocrlf false
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install cygwin base packages with chocolatey
|
||||
run: |
|
||||
choco config get cacheLocation
|
||||
|
|
@ -178,14 +176,9 @@ 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: bash
|
||||
shell: cmd
|
||||
run: |
|
||||
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"
|
||||
echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin >> %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
|
||||
|
|
@ -231,17 +224,15 @@ jobs:
|
|||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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
|
||||
sync: nfs
|
||||
copyback: false
|
||||
run: |
|
||||
if [ "${{ secrets.TokenName1}}" ] ; then
|
||||
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
|
||||
|
|
@ -260,76 +251,13 @@ 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"
|
||||
|
||||
|
||||
|
||||
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 }}
|
||||
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@v7
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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
|
||||
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"
|
||||
|
||||
|
||||
|
||||
OpenBSD:
|
||||
runs-on: ubuntu-latest
|
||||
needs: GhostBSD
|
||||
needs: FreeBSD
|
||||
env:
|
||||
TEST_DNS : ${{ secrets.TEST_DNS }}
|
||||
TestingDomain: ${{ secrets.TestingDomain }}
|
||||
|
|
@ -347,17 +275,15 @@ jobs:
|
|||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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_add socat curl
|
||||
usesh: true
|
||||
sync: nfs
|
||||
copyback: false
|
||||
run: |
|
||||
if [ "${{ secrets.TokenName1}}" ] ; then
|
||||
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
|
||||
|
|
@ -376,11 +302,7 @@ 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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -404,18 +326,16 @@ jobs:
|
|||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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
|
||||
sync: nfs
|
||||
copyback: false
|
||||
run: |
|
||||
if [ "${{ secrets.TokenName1}}" ] ; then
|
||||
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
|
||||
|
|
@ -434,11 +354,7 @@ 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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -462,19 +378,16 @@ jobs:
|
|||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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 libnghttp2
|
||||
pkg install -y curl socat
|
||||
pkg install -y curl socat libnghttp2
|
||||
usesh: true
|
||||
sync: nfs
|
||||
copyback: false
|
||||
run: |
|
||||
if [ "${{ secrets.TokenName1}}" ] ; then
|
||||
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
|
||||
|
|
@ -493,77 +406,16 @@ 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@v7
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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: MidnightBSD
|
||||
needs: DragonFlyBSD
|
||||
env:
|
||||
TEST_DNS : ${{ secrets.TEST_DNS }}
|
||||
TestingDomain: ${{ secrets.TestingDomain }}
|
||||
|
|
@ -582,18 +434,14 @@ jobs:
|
|||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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: |
|
||||
pkgutil -U
|
||||
pkgutil -y -i socat
|
||||
copyback: false
|
||||
prepare: pkgutil -y -i socat
|
||||
run: |
|
||||
pkg set-mediator -v -I default@1.1 openssl
|
||||
export PATH=/usr/gnu/bin:$PATH
|
||||
|
|
@ -614,11 +462,6 @@ 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:
|
||||
|
|
@ -642,15 +485,13 @@ jobs:
|
|||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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
|
||||
copyback: false
|
||||
prepare: pkg install socat
|
||||
run: |
|
||||
if [ "${{ secrets.TokenName1}}" ] ; then
|
||||
|
|
@ -670,306 +511,5 @@ 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@v7
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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@v7
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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@v7
|
||||
- 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 }}
|
||||
cache-after-prepare: true
|
||||
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"
|
||||
|
||||
|
||||
|
||||
Hurd:
|
||||
runs-on: ubuntu-latest
|
||||
needs: Haiku
|
||||
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
|
||||
TokenName1: ${{ secrets.TokenName1}}
|
||||
TokenName2: ${{ secrets.TokenName2}}
|
||||
TokenName3: ${{ secrets.TokenName3}}
|
||||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- uses: vmactions/hurd-vm@v1
|
||||
with:
|
||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
||||
cache-after-prepare: true
|
||||
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
|
||||
usesh: true
|
||||
prepare: |
|
||||
apt-get update -y
|
||||
apt-get install -y curl cron
|
||||
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"
|
||||
|
||||
|
||||
|
||||
OpenEuler:
|
||||
runs-on: ubuntu-latest
|
||||
needs: Hurd
|
||||
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
|
||||
TokenName1: ${{ secrets.TokenName1}}
|
||||
TokenName2: ${{ secrets.TokenName2}}
|
||||
TokenName3: ${{ secrets.TokenName3}}
|
||||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- uses: vmactions/openeuler-vm@v1
|
||||
with:
|
||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
||||
cache-after-prepare: true
|
||||
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
|
||||
usesh: true
|
||||
prepare: dnf install -y curl socat cronie tar gzip
|
||||
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"
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
21
.github/workflows/DragonFlyBSD.yml
vendored
21
.github/workflows/DragonFlyBSD.yml
vendored
|
|
@ -31,8 +31,8 @@ jobs:
|
|||
CA_EMAIL: ""
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
#- TEST_ACME_Server: "ZeroSSL.com"
|
||||
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
# CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
|
||||
# CA: "ZeroSSL RSA Domain Secure Site CA"
|
||||
# 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@v6
|
||||
- uses: anyvm-org/cf-tunnel@v0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: vmactions/cf-tunnel@v0
|
||||
id: tunnel
|
||||
with:
|
||||
protocol: http
|
||||
|
|
@ -57,22 +57,15 @@ 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 }}
|
||||
cache-after-prepare: true
|
||||
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 libnghttp2
|
||||
pkg install -y curl socat
|
||||
pkg install -y curl socat libnghttp2
|
||||
usesh: true
|
||||
sync: nfs
|
||||
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"
|
||||
|
||||
|
||||
|
|
|
|||
18
.github/workflows/FreeBSD.yml
vendored
18
.github/workflows/FreeBSD.yml
vendored
|
|
@ -37,8 +37,8 @@ jobs:
|
|||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
ACME_USE_WGET: 1
|
||||
#- TEST_ACME_Server: "ZeroSSL.com"
|
||||
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
# CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
|
||||
# CA: "ZeroSSL RSA Domain Secure Site CA"
|
||||
# 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@v6
|
||||
- uses: anyvm-org/cf-tunnel@v0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: vmactions/cf-tunnel@v0
|
||||
id: tunnel
|
||||
with:
|
||||
protocol: http
|
||||
|
|
@ -63,20 +63,14 @@ 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 }}
|
||||
cache-after-prepare: true
|
||||
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
|
||||
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"
|
||||
|
||||
|
||||
|
|
|
|||
83
.github/workflows/GhostBSD.yml
vendored
83
.github/workflows/GhostBSD.yml
vendored
|
|
@ -1,83 +0,0 @@
|
|||
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 }}
|
||||
cache-after-prepare: true
|
||||
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"
|
||||
85
.github/workflows/Haiku.yml
vendored
85
.github/workflows/Haiku.yml
vendored
|
|
@ -1,85 +0,0 @@
|
|||
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 }}
|
||||
cache-after-prepare: true
|
||||
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"
|
||||
|
||||
76
.github/workflows/Hurd.yml
vendored
76
.github/workflows/Hurd.yml
vendored
|
|
@ -1,76 +0,0 @@
|
|||
name: Hurd
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Hurd.yml'
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Hurd.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
Hurd:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||
CA_ECDSA: ""
|
||||
CA: ""
|
||||
CA_EMAIL: ""
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
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 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- 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/hurd-vm@v1
|
||||
with:
|
||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
||||
cache-after-prepare: true
|
||||
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
|
||||
nat: |
|
||||
"8080": "80"
|
||||
# Do NOT install socat: socat's SYSTEM: address is broken on GNU Hurd
|
||||
# (the child shell output goes to socat's stdout instead of the socket,
|
||||
# so clients get an empty reply). Without socat, acme.sh standalone
|
||||
# mode falls back to its python3 server, which works on Hurd.
|
||||
prepare: |
|
||||
apt-get update -y
|
||||
apt-get install -y curl cron
|
||||
usesh: true
|
||||
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"
|
||||
11
.github/workflows/Linux.yml
vendored
11
.github/workflows/Linux.yml
vendored
|
|
@ -26,21 +26,14 @@ jobs:
|
|||
Linux:
|
||||
strategy:
|
||||
matrix:
|
||||
os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "gentoo/stage3"]
|
||||
os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "mageia", "gentoo/stage3"]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_LOCAL: 1
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||
steps:
|
||||
- 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
|
||||
- uses: actions/checkout@v4
|
||||
- name: Clone acmetest
|
||||
run: |
|
||||
cd .. \
|
||||
|
|
|
|||
13
.github/workflows/MacOS.yml
vendored
13
.github/workflows/MacOS.yml
vendored
|
|
@ -31,8 +31,8 @@ jobs:
|
|||
CA_EMAIL: ""
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
#- TEST_ACME_Server: "ZeroSSL.com"
|
||||
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
# CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
|
||||
# CA: "ZeroSSL RSA Domain Secure Site CA"
|
||||
# CA_EMAIL: "githubtest@acme.sh"
|
||||
# TEST_PREFERRED_CHAIN: ""
|
||||
runs-on: macos-latest
|
||||
|
|
@ -44,16 +44,9 @@ jobs:
|
|||
CA_EMAIL: ${{ matrix.CA_EMAIL }}
|
||||
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- 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 .. \
|
||||
|
|
|
|||
75
.github/workflows/MidnightBSD.yml
vendored
75
.github/workflows/MidnightBSD.yml
vendored
|
|
@ -1,75 +0,0 @@
|
|||
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 }}
|
||||
cache-after-prepare: true
|
||||
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"
|
||||
20
.github/workflows/NetBSD.yml
vendored
20
.github/workflows/NetBSD.yml
vendored
|
|
@ -31,8 +31,8 @@ jobs:
|
|||
CA_EMAIL: ""
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
#- TEST_ACME_Server: "ZeroSSL.com"
|
||||
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
# CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
|
||||
# CA: "ZeroSSL RSA Domain Secure Site CA"
|
||||
# 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@v6
|
||||
- uses: anyvm-org/cf-tunnel@v0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: vmactions/cf-tunnel@v0
|
||||
id: tunnel
|
||||
with:
|
||||
protocol: http
|
||||
|
|
@ -57,21 +57,15 @@ 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 }}
|
||||
cache-after-prepare: true
|
||||
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
|
||||
sync: nfs
|
||||
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"
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
66
.github/workflows/Nginx.yml
vendored
66
.github/workflows/Nginx.yml
vendored
|
|
@ -1,66 +0,0 @@
|
|||
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
|
||||
18
.github/workflows/Omnios.yml
vendored
18
.github/workflows/Omnios.yml
vendored
|
|
@ -37,8 +37,8 @@ jobs:
|
|||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
ACME_USE_WGET: 1
|
||||
#- TEST_ACME_Server: "ZeroSSL.com"
|
||||
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
# CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
|
||||
# CA: "ZeroSSL RSA Domain Secure Site CA"
|
||||
# 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@v6
|
||||
- uses: anyvm-org/cf-tunnel@v0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: vmactions/cf-tunnel@v0
|
||||
id: tunnel
|
||||
with:
|
||||
protocol: http
|
||||
|
|
@ -63,19 +63,13 @@ 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 }}
|
||||
cache-after-prepare: true
|
||||
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
|
||||
sync: nfs
|
||||
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"
|
||||
|
||||
|
||||
|
|
|
|||
18
.github/workflows/OpenBSD.yml
vendored
18
.github/workflows/OpenBSD.yml
vendored
|
|
@ -37,8 +37,8 @@ jobs:
|
|||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
ACME_USE_WGET: 1
|
||||
#- TEST_ACME_Server: "ZeroSSL.com"
|
||||
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
# CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
|
||||
# CA: "ZeroSSL RSA Domain Secure Site CA"
|
||||
# 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@v6
|
||||
- uses: anyvm-org/cf-tunnel@v0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: vmactions/cf-tunnel@v0
|
||||
id: tunnel
|
||||
with:
|
||||
protocol: http
|
||||
|
|
@ -63,20 +63,14 @@ 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 }}
|
||||
cache-after-prepare: true
|
||||
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
|
||||
sync: nfs
|
||||
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"
|
||||
|
||||
|
||||
|
|
|
|||
70
.github/workflows/OpenEuler.yml
vendored
70
.github/workflows/OpenEuler.yml
vendored
|
|
@ -1,70 +0,0 @@
|
|||
name: OpenEuler
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/OpenEuler.yml'
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/OpenEuler.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
OpenEuler:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||
CA_ECDSA: ""
|
||||
CA: ""
|
||||
CA_EMAIL: ""
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
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 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- 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/openeuler-vm@v1
|
||||
with:
|
||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
||||
cache-after-prepare: true
|
||||
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
|
||||
nat: |
|
||||
"8080": "80"
|
||||
prepare: dnf install -y curl socat cronie tar gzip
|
||||
usesh: true
|
||||
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"
|
||||
81
.github/workflows/OpenIndiana.yml
vendored
81
.github/workflows/OpenIndiana.yml
vendored
|
|
@ -1,81 +0,0 @@
|
|||
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 }}
|
||||
cache-after-prepare: true
|
||||
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"
|
||||
|
||||
6
.github/workflows/PebbleStrict.yml
vendored
6
.github/workflows/PebbleStrict.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
TEST_CA: "Pebble Intermediate CA"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y socat
|
||||
- name: Run Pebble
|
||||
|
|
@ -58,14 +58,14 @@ jobs:
|
|||
TEST_IPCERT: 1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y socat
|
||||
- name: Run Pebble
|
||||
run: |
|
||||
docker run --rm -itd --name=pebble \
|
||||
-e PEBBLE_VA_ALWAYS_VALID=1 \
|
||||
-p 14000:14000 -p 15000:15000 ghcr.io/letsencrypt/pebble:latest -config /test/config/pebble-config.json -strict
|
||||
-p 14000:14000 -p 15000:15000 letsencrypt/pebble:latest pebble -config /test/config/pebble-config.json -strict
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
|
|
|
|||
22
.github/workflows/Solaris.yml
vendored
22
.github/workflows/Solaris.yml
vendored
|
|
@ -37,8 +37,8 @@ jobs:
|
|||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
ACME_USE_WGET: 1
|
||||
#- TEST_ACME_Server: "ZeroSSL.com"
|
||||
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
# CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
|
||||
# CA: "ZeroSSL RSA Domain Secure Site CA"
|
||||
# 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@v6
|
||||
- uses: anyvm-org/cf-tunnel@v0
|
||||
- uses: actions/checkout@v4
|
||||
- uses: vmactions/cf-tunnel@v0
|
||||
id: tunnel
|
||||
with:
|
||||
protocol: http
|
||||
|
|
@ -63,21 +63,13 @@ 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 }}
|
||||
cache-after-prepare: true
|
||||
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||
nat: |
|
||||
"8080": "80"
|
||||
prepare: |
|
||||
pkgutil -U
|
||||
pkgutil -y -i socat curl wget
|
||||
sync: nfs
|
||||
prepare: pkgutil -y -i socat curl wget
|
||||
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"
|
||||
|
||||
|
||||
|
|
|
|||
80
.github/workflows/Tribblix.yml
vendored
80
.github/workflows/Tribblix.yml
vendored
|
|
@ -1,80 +0,0 @@
|
|||
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 }}
|
||||
cache-after-prepare: true
|
||||
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"
|
||||
6
.github/workflows/Ubuntu.yml
vendored
6
.github/workflows/Ubuntu.yml
vendored
|
|
@ -37,8 +37,8 @@ jobs:
|
|||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
ACME_USE_WGET: 1
|
||||
- TEST_ACME_Server: "ZeroSSL.com"
|
||||
CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
|
||||
CA: "ZeroSSL RSA Domain Secure Site CA"
|
||||
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@v6
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y socat wget
|
||||
- name: Start StepCA
|
||||
|
|
|
|||
13
.github/workflows/Windows.yml
vendored
13
.github/workflows/Windows.yml
vendored
|
|
@ -31,8 +31,8 @@ jobs:
|
|||
CA_EMAIL: ""
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
#- TEST_ACME_Server: "ZeroSSL.com"
|
||||
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
# CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
|
||||
# CA: "ZeroSSL RSA Domain Secure Site CA"
|
||||
# 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@v6
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install cygwin base packages with chocolatey
|
||||
run: |
|
||||
choco config get cacheLocation
|
||||
|
|
@ -67,13 +67,6 @@ 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/
|
||||
|
|
|
|||
114
.github/workflows/blacklist-command.yml
vendored
114
.github/workflows/blacklist-command.yml
vendored
|
|
@ -1,114 +0,0 @@
|
|||
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"
|
||||
});
|
||||
37
.github/workflows/dockerhub.yml
vendored
37
.github/workflows/dockerhub.yml
vendored
|
|
@ -15,8 +15,6 @@ concurrency:
|
|||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
DOCKER_IMAGE: neilpang/acme.sh
|
||||
|
||||
jobs:
|
||||
CheckToken:
|
||||
|
|
@ -41,31 +39,20 @@ 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@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${DOCKER_IMAGE}
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- 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: |
|
||||
DOCKER_IMAGE=neilpang/acme.sh
|
||||
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
DOCKER_IMAGE_TAG=${GITHUB_REF#refs/tags/}
|
||||
fi
|
||||
|
|
@ -79,22 +66,8 @@ jobs:
|
|||
fi
|
||||
fi
|
||||
|
||||
echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV"
|
||||
|
||||
DOCKER_LABELS=()
|
||||
while read -r label; do
|
||||
DOCKER_LABELS+=(--label "${label}")
|
||||
done <<<"${DOCKER_METADATA_OUTPUT_LABELS}"
|
||||
|
||||
docker buildx build \
|
||||
--tag ${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
|
||||
"${DOCKER_LABELS[@]}" \
|
||||
--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"
|
||||
|
|
|
|||
120
.github/workflows/issue.yml
vendored
120
.github/workflows/issue.yml
vendored
|
|
@ -2,128 +2,18 @@ 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@v9
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
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,
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.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. Before posting the log, review it and REDACT any secrets: private keys (`-----BEGIN ... PRIVATE KEY-----` blocks), API tokens and passwords."
|
||||
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."
|
||||
|
||||
})
|
||||
16
.github/workflows/pr_dns.yml
vendored
16
.github/workflows/pr_dns.yml
vendored
|
|
@ -11,9 +11,8 @@ on:
|
|||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- uses: actions/github-script@v9
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
|
|
@ -21,14 +20,11 @@ jobs:
|
|||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `**Welcome**
|
||||
READ ME !!!!!
|
||||
Read me !!!!!!
|
||||
First thing: don't send PR to the master branch, please send to the dev branch instead.
|
||||
Please read the [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide).
|
||||
You MUST pass the [DNS-API-Test](../wiki/DNS-API-Test).
|
||||
Then reply on this message, otherwise, your code will not be reviewed or merged.
|
||||
Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2
|
||||
注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试.
|
||||
First thing: don't send PR to the master branch, please send to the dev branch instead.
|
||||
Please make sure you've read our [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide) and [DNS-API-Test](../wiki/DNS-API-Test).
|
||||
Then reply on this message, otherwise, your code will not be reviewed or merged.
|
||||
We look forward to reviewing your Pull request shortly ✨
|
||||
注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试.
|
||||
`
|
||||
})
|
||||
|
||||
|
|
|
|||
5
.github/workflows/pr_notify.yml
vendored
5
.github/workflows/pr_notify.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Check notify api
|
||||
name: Check dns api
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
|
|
@ -13,9 +13,8 @@ on:
|
|||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- uses: actions/github-script@v9
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
|
|
|
|||
110
.github/workflows/revert-command.yml
vendored
110
.github/workflows/revert-command.yml
vendored
|
|
@ -1,110 +0,0 @@
|
|||
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"
|
||||
});
|
||||
4
.github/workflows/shellcheck.yml
vendored
4
.github/workflows/shellcheck.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
ShellCheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- 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@v6
|
||||
- uses: actions/checkout@v4
|
||||
- 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
|
||||
|
|
|
|||
32
.github/workflows/vtag.yml
vendored
32
.github/workflows/vtag.yml
vendored
|
|
@ -1,32 +0,0 @@
|
|||
name: Mirror version tag
|
||||
|
||||
# Historical release tags are plain version numbers ("3.1.3") and cannot be
|
||||
# renamed. When a plain version tag is pushed (including the tag created by
|
||||
# publishing a GitHub release), mirror it as a "v"-prefixed tag ("v3.1.3")
|
||||
# pointing to the same object, so both forms exist.
|
||||
# No retrigger loop: the tag filter never matches a "v"-prefixed tag, and
|
||||
# refs created with GITHUB_TOKEN do not fire workflows anyway.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
vtag:
|
||||
if: github.repository == 'acmesh-official/acme.sh'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create the v-prefixed tag
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if gh api "repos/${{ github.repository }}/git/ref/tags/v${{ github.ref_name }}" >/dev/null 2>&1; then
|
||||
echo "Tag v${{ github.ref_name }} already exists, nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
gh api "repos/${{ github.repository }}/git/refs" -f ref="refs/tags/v${{ github.ref_name }}" -f sha="${{ github.sha }}"
|
||||
echo "Created tag v${{ github.ref_name }} -> ${{ github.sha }}"
|
||||
325
.github/workflows/wiki-guard.yml
vendored
325
.github/workflows/wiki-guard.yml
vendored
|
|
@ -1,325 +0,0 @@
|
|||
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
|
||||
82
.github/workflows/wiki-monitor.yml
vendored
82
.github/workflows/wiki-monitor.yml
vendored
|
|
@ -1,82 +0,0 @@
|
|||
name: Notify via Issue on Wiki Edit
|
||||
|
||||
on:
|
||||
gollum:
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- name: Checkout wiki repository
|
||||
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")
|
||||
page_name=$(jq -r '.pages[0].page_name' "$GITHUB_EVENT_PATH")
|
||||
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
|
||||
else
|
||||
echo "(no diff)" > ../wiki.diff
|
||||
fi
|
||||
cd ..
|
||||
{
|
||||
echo "Wiki edited"
|
||||
echo -n "User: "
|
||||
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'
|
||||
cat wiki.diff
|
||||
echo '```'
|
||||
} > wiki-change-msg.txt
|
||||
echo "notify=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create issue to notify Neilpang
|
||||
if: steps.msg.outputs.notify == 'true'
|
||||
uses: peter-evans/create-issue-from-file@v6
|
||||
with:
|
||||
title: "Wiki edited"
|
||||
content-filepath: ./wiki-change-msg.txt
|
||||
assignees: Neilpang
|
||||
env:
|
||||
TZ: Asia/Shanghai
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# 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.
|
||||
38
Dockerfile
38
Dockerfile
|
|
@ -1,4 +1,4 @@
|
|||
FROM alpine:3.23
|
||||
FROM alpine:3.17
|
||||
|
||||
RUN apk --no-cache add -f \
|
||||
openssl \
|
||||
|
|
@ -13,32 +13,20 @@ RUN apk --no-cache add -f \
|
|||
tar \
|
||||
libidn \
|
||||
jq \
|
||||
yq-go \
|
||||
supercronic
|
||||
cronie
|
||||
|
||||
ENV LE_WORKING_DIR=/acmebin
|
||||
|
||||
ENV LE_CONFIG_HOME=/acme.sh
|
||||
|
||||
ENV HOME=/acme.sh
|
||||
ENV LE_CONFIG_HOME /acme.sh
|
||||
|
||||
ARG AUTO_UPGRADE=1
|
||||
|
||||
ENV AUTO_UPGRADE=$AUTO_UPGRADE
|
||||
ENV AUTO_UPGRADE $AUTO_UPGRADE
|
||||
|
||||
#Install
|
||||
COPY ./acme.sh /install_acme.sh/acme.sh
|
||||
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
|
||||
|
||||
COPY ./ /install_acme.sh/
|
||||
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 chown -R acme:acme $LE_CONFIG_HOME
|
||||
RUN ln -s /root/.acme.sh/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 for verb in help \
|
||||
version \
|
||||
|
|
@ -72,23 +60,15 @@ RUN for verb in help \
|
|||
set-default-ca \
|
||||
set-default-chain \
|
||||
; do \
|
||||
printf -- "%b" "#!/usr/bin/env sh\n$LE_WORKING_DIR/acme.sh --${verb} --config-home $LE_CONFIG_HOME \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \
|
||||
printf -- "%b" "#!/usr/bin/env sh\n/root/.acme.sh/acme.sh --${verb} --config-home /acme.sh \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \
|
||||
; done
|
||||
|
||||
RUN printf "%b" '#!'"/usr/bin/env sh\n \
|
||||
if [ \"\$1\" = \"daemon\" ]; then \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 \
|
||||
exec crond -n -s -m off \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
|
||||
fi\n" >/entry.sh && chmod +x /entry.sh
|
||||
|
||||
VOLUME /acme.sh
|
||||
|
||||
|
|
|
|||
553
README.md
553
README.md
|
|
@ -1,91 +1,52 @@
|
|||
<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>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg" alt="Hurd"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg" alt="OpenEuler"></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
|
||||
|
||||
> 💡 It's probably the **easiest & smartest** shell script to automatically issue & renew free certificates.
|
||||
- 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.
|
||||
|
||||
<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>
|
||||
It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates.
|
||||
|
||||
---
|
||||
Wiki: https://github.com/acmesh-official/acme.sh/wiki
|
||||
|
||||
## 🌏 [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
|
||||
For Docker Fans: [acme.sh :two_hearts: Docker ](https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker)
|
||||
|
||||
---
|
||||
Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|
||||
|
||||
## 🏆 Who Uses acme.sh?
|
||||
|
||||
# [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
|
||||
|
||||
# Who:
|
||||
- [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)
|
||||
|
|
@ -99,9 +60,7 @@
|
|||
- [lnmp.org](https://lnmp.org/)
|
||||
- [more...](https://github.com/acmesh-official/acme.sh/wiki/Blogs-and-tutorials)
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Tested OS
|
||||
# Tested OS
|
||||
|
||||
| NO | Status| Platform|
|
||||
|----|-------|---------|
|
||||
|
|
@ -114,83 +73,67 @@
|
|||
|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/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
|
||||
|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
|
||||
|11|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|ClearLinux
|
||||
|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
|
||||
|28|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd
|
||||
|29|[](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml)|openEuler
|
||||
|
||||
|
||||
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
|
||||
>
|
||||
> 🖥️ The testing VMs are supported by [vmactions.org](https://vmactions.org)
|
||||
Check our [testing project](https://github.com/acmesh-official/acmetest):
|
||||
|
||||
---
|
||||
https://github.com/acmesh-official/acmetest
|
||||
|
||||
## 🏛️ Supported CA
|
||||
# Supported 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 |
|
||||
- [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA)(default)
|
||||
- Letsencrypt.org CA
|
||||
- [BuyPass.com CA](https://github.com/acmesh-official/acme.sh/wiki/BuyPass.com-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)
|
||||
- [Pebble strict Mode](https://github.com/letsencrypt/pebble)
|
||||
- Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA
|
||||
|
||||
---
|
||||
# Supported modes
|
||||
|
||||
## ⚙️ 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)
|
||||
|
||||
| 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
|
||||
|
||||
## 📖 Usage Guide
|
||||
### 1. Install online
|
||||
|
||||
### 1️⃣ How to Install
|
||||
|
||||
#### 📥 Install Online
|
||||
|
||||
> Check this project: https://github.com/acmesh-official/get.acme.sh
|
||||
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
|
||||
```
|
||||
|
||||
#### 📦 Install from Git
|
||||
|
||||
### 2. Or, Install from git
|
||||
|
||||
Clone this project and launch installation:
|
||||
|
||||
|
|
@ -200,11 +143,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.
|
||||
|
|
@ -217,19 +160,17 @@ 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.
|
||||
|
||||
✅ **You are ready to issue certs now!**
|
||||
Ok, you are ready to issue certs now.
|
||||
|
||||
**Show help message:**
|
||||
Show help message:
|
||||
|
||||
```sh
|
||||
acme.sh -h
|
||||
root@v1:~# acme.sh -h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ Issue a Certificate
|
||||
# 2. Just issue a cert
|
||||
|
||||
**Example 1:** Single domain.
|
||||
|
||||
|
|
@ -264,21 +205,17 @@ 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 **30** days.
|
||||
The certs will be renewed automatically every **60** days.
|
||||
|
||||
> 🔐 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 Certificate to Apache/Nginx
|
||||
# 3. Install the cert to Apache/Nginx etc.
|
||||
|
||||
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.
|
||||
|
||||
> ⚠️ **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:
|
||||
**Apache** example:
|
||||
```bash
|
||||
acme.sh --install-cert -d example.com \
|
||||
--cert-file /path/to/certfile/in/apache/cert.pem \
|
||||
|
|
@ -287,7 +224,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 \
|
||||
|
|
@ -301,89 +238,91 @@ 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 **30** days by default (configurable). Once renewed, the Apache/Nginx service will be reloaded automatically.
|
||||
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`.
|
||||
|
||||
> ⚠️ **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 Certificate
|
||||
# 4. Use Standalone server to issue cert
|
||||
|
||||
> 🔐 Requires root/sudoer or permission to listen on port **80** (TCP)
|
||||
**(requires you to be root/sudoer or have 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
|
||||
|
||||
### 5️⃣ Use Standalone TLS Server to Issue Certificate
|
||||
**(requires you to be root/sudoer or have permission to listen on port 443 (TCP))**
|
||||
|
||||
> 🔐 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.
|
||||
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 root/sudoer to interact with Apache server
|
||||
**(requires you to be root/sudoer, since it is required 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
|
||||
```
|
||||
|
||||
> 💡 **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!
|
||||
**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
|
||||
|
||||
### 7️⃣ Use Nginx Mode
|
||||
|
||||
> 🔐 Requires root/sudoer to interact with Nginx server
|
||||
**(requires you to be root/sudoer, since it is required 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```sh
|
||||
acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com
|
||||
```
|
||||
|
||||
> 💡 **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!
|
||||
**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 DNS providers:** https://github.com/acmesh-official/acme.sh/wiki/dnsapi
|
||||
### Currently acme.sh supports most of the dns providers:
|
||||
|
||||
---
|
||||
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.
|
||||
|
||||
|
|
@ -413,154 +352,70 @@ Then just rerun with `renew` argument:
|
|||
acme.sh --renew -d example.com
|
||||
```
|
||||
|
||||
✅ **Done!**
|
||||
Ok, it's done.
|
||||
|
||||
> ⚠️ **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.**
|
||||
**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.**
|
||||
|
||||
---
|
||||
**Please use dns api mode instead.**
|
||||
|
||||
### 🔟 Use DNS Persist Mode
|
||||
# 10. Issue ECC certificates
|
||||
|
||||
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode
|
||||
Just set the `keylength` parameter with a prefix `ec-`.
|
||||
|
||||
📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)
|
||||
For example:
|
||||
|
||||
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.
|
||||
|
||||
#### 🪄 Step 1: Print the TXT record value
|
||||
### Single domain ECC 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]
|
||||
acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-256
|
||||
```
|
||||
|
||||
Options:
|
||||
### SAN multi domain ECC certificate
|
||||
|
||||
| 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. |
|
||||
```bash
|
||||
acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength ec-256
|
||||
```
|
||||
|
||||
You should get an output like:
|
||||
Please look at the `keylength` parameter above.
|
||||
|
||||
Valid values are:
|
||||
|
||||
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)**
|
||||
|
||||
|
||||
# 11. Issue Wildcard certificates
|
||||
|
||||
It's simple, just give a wildcard domain as the `-d` parameter.
|
||||
|
||||
```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
|
||||
|
||||
```bash
|
||||
acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
|
||||
### 1️⃣3️⃣ How to Renew Certificates
|
||||
No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days.
|
||||
|
||||
> 🔄 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:
|
||||
However, you can also force to renew a cert:
|
||||
|
||||
```sh
|
||||
acme.sh --renew -d example.com --force
|
||||
```
|
||||
|
||||
**For ECC cert:**
|
||||
or, for ECC cert:
|
||||
|
||||
```sh
|
||||
acme.sh --renew -d example.com --force --ecc
|
||||
```
|
||||
|
||||
#### 📡 ACME Renewal Information (ARI) — RFC 9773
|
||||
|
||||
📖 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
|
||||
# 13. How to stop cert renewal
|
||||
|
||||
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
|
||||
|
||||
|
|
@ -570,80 +425,73 @@ 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`) manually.
|
||||
You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣5️⃣ How to Upgrade acme.sh
|
||||
# 14. How to upgrade `acme.sh`
|
||||
|
||||
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
|
||||
acme.sh is in constant development, so it's strongly recommended to use the latest code.
|
||||
|
||||
**Update to latest:**
|
||||
You can update acme.sh to the latest code:
|
||||
|
||||
```sh
|
||||
acme.sh --upgrade
|
||||
```
|
||||
|
||||
**Enable auto upgrade:**
|
||||
You can also enable auto upgrade:
|
||||
|
||||
```sh
|
||||
acme.sh --upgrade --auto-upgrade
|
||||
```
|
||||
|
||||
**Disable auto upgrade:**
|
||||
Then **acme.sh** will be kept up to date automatically.
|
||||
|
||||
Disable auto upgrade:
|
||||
|
||||
```sh
|
||||
acme.sh --upgrade --auto-upgrade 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣6️⃣ Issue a Certificate from an Existing CSR
|
||||
# 15. Issue a cert 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
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣7️⃣ Send Notifications in Cronjob
|
||||
# 16. Send notifications in cronjob
|
||||
|
||||
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
|
||||
https://github.com/acmesh-official/acme.sh/wiki/notify
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣8️⃣ Under the Hood
|
||||
# 17. 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
|
||||
|
||||
| Project | Link |
|
||||
|---------|------|
|
||||
| 🙏 Acme-tiny | https://github.com/diafygi/acme-tiny |
|
||||
| 📜 ACME protocol | https://github.com/ietf-wg-acme/acme |
|
||||
# 18. Acknowledgments
|
||||
|
||||
---
|
||||
1. Acme-tiny: https://github.com/diafygi/acme-tiny
|
||||
2. ACME protocol: https://github.com/ietf-wg-acme/acme
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
### 💻 Code Contributors
|
||||
## 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>
|
||||
|
||||
If you want to become a contributor make sure to read [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
### 💰 Financial Contributors
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)]
|
||||
|
||||
#### 👤 Individuals
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/individuals.svg?width=890"></a>
|
||||
|
||||
#### 🏢 Organizations
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/acmesh/contribute)]
|
||||
|
||||
|
|
@ -658,43 +506,20 @@ 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
|
||||
|
||||
📄 **License:** GPLv3
|
||||
# 19. License & Others
|
||||
|
||||
⭐ Please **Star** and **Fork** this project!
|
||||
License is GPLv3
|
||||
|
||||
🐛 [Issues](https://github.com/acmesh-official/acme.sh/issues) and 🔀 [Pull Requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome.
|
||||
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.
|
||||
|
||||
### 2️⃣1️⃣ Donate
|
||||
|
||||
> 💝 Your donation makes **acme.sh** better!
|
||||
# 20. Donate
|
||||
Your donation makes **acme.sh** better:
|
||||
|
||||
| Method | Link |
|
||||
|--------|------|
|
||||
| PayPal / Alipay(支付宝) / Wechat(微信) | [https://donate.acme.sh/](https://donate.acme.sh/) |
|
||||
1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)
|
||||
|
||||
📜 [Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)
|
||||
|
||||
---
|
||||
|
||||
### 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!
|
||||
> For more information about our services, including free and paid SSL/TLS certificates, visit https://zerossl.com.
|
||||
>
|
||||
> 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">
|
||||
<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">
|
||||
<img src="https://zerossl.com/assets/images/zerossl_logo.svg" alt="ZeroSSL" width="256">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
[Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)
|
||||
|
|
|
|||
|
|
@ -1,341 +0,0 @@
|
|||
# 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,23 +1,17 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034,SC2154
|
||||
|
||||
# Script to create certificate to Alibaba Cloud CDN
|
||||
#
|
||||
# Docs: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#33-deploy-your-certificate-to-cdn-or-dcdn-of-alibaba-cloud-aliyun
|
||||
#
|
||||
# This deployment required following variables
|
||||
# export Ali_Key="ALIACCESSKEY"
|
||||
# export Ali_Secret="ALISECRETKEY"
|
||||
# The credentials are shared with all the Alibaba Cloud deploy hooks and dnsapi
|
||||
#
|
||||
# To specify the CDN domain that is different from the certificate CN, usually used for multi-domain or wildcard certificates
|
||||
# export DEPLOY_ALI_CDN_DOMAIN="cdn.example.com"
|
||||
# If you have multiple CDN domains using the same certificate, just
|
||||
# If you have more than one domain, just
|
||||
# export DEPLOY_ALI_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
|
||||
#
|
||||
# For DCDN, see ali_dcdn deploy hook
|
||||
# The credentials are shared with all domains, also shared with dns_ali api
|
||||
|
||||
Ali_CDN_API="https://cdn.aliyuncs.com/"
|
||||
Ali_API="https://cdn.aliyuncs.com/"
|
||||
|
||||
ali_cdn_deploy() {
|
||||
_cdomain="$1"
|
||||
|
|
@ -32,16 +26,18 @@ ali_cdn_deploy() {
|
|||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
# Load dnsapi/dns_ali.sh to reduce the duplicated codes
|
||||
# https://github.com/acmesh-official/acme.sh/pull/5205#issuecomment-2357867276
|
||||
dnsapi_ali="$(_findHook "$_cdomain" "$_SUB_FOLDER_DNSAPI" dns_ali)"
|
||||
# shellcheck source=/dev/null
|
||||
if ! . "$dnsapi_ali"; then
|
||||
_err "Error loading file $dnsapi_ali. Please check your API file and try again."
|
||||
Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
|
||||
Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
|
||||
if [ -z "$Ali_Key" ] || [ -z "$Ali_Secret" ]; then
|
||||
Ali_Key=""
|
||||
Ali_Secret=""
|
||||
_err "You don't specify aliyun api key and secret yet."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_prepare_ali_credentials || return 1
|
||||
#save the api key and secret to the account conf file.
|
||||
_saveaccountconf_mutable Ali_Key "$Ali_Key"
|
||||
_saveaccountconf_mutable Ali_Secret "$Ali_Secret"
|
||||
|
||||
_getdeployconf DEPLOY_ALI_CDN_DOMAIN
|
||||
if [ "$DEPLOY_ALI_CDN_DOMAIN" ]; then
|
||||
|
|
@ -51,8 +47,8 @@ ali_cdn_deploy() {
|
|||
fi
|
||||
|
||||
# read cert and key files and urlencode both
|
||||
_cert=$(_url_encode upper-hex <"$_cfullchain")
|
||||
_key=$(_url_encode upper-hex <"$_ckey")
|
||||
_cert=$(_url_encode_upper <"$_cfullchain")
|
||||
_key=$(_url_encode_upper <"$_ckey")
|
||||
|
||||
_debug2 _cert "$_cert"
|
||||
_debug2 _key "$_key"
|
||||
|
|
@ -68,9 +64,82 @@ ali_cdn_deploy() {
|
|||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# act ign mtd
|
||||
_ali_rest() {
|
||||
act="$1"
|
||||
ign="$2"
|
||||
mtd="$3"
|
||||
|
||||
signature=$(printf "%s" "$mtd&%2F&$(_ali_urlencode "$query")" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
|
||||
signature=$(_ali_urlencode "$signature")
|
||||
url="$Ali_API?$query&Signature=$signature"
|
||||
|
||||
if [ "$mtd" = "GET" ]; then
|
||||
response="$(_get "$url")"
|
||||
else
|
||||
# post payload is not supported yet because of signature
|
||||
response="$(_post "" "$url")"
|
||||
fi
|
||||
|
||||
_ret="$?"
|
||||
_debug2 response "$response"
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Error <$act>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$ign" ]; then
|
||||
message="$(echo "$response" | _egrep_o "\"Message\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")"
|
||||
if [ "$message" ]; then
|
||||
_err "$message"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_ali_urlencode() {
|
||||
_str="$1"
|
||||
_str_len=${#_str}
|
||||
_u_i=1
|
||||
while [ "$_u_i" -le "$_str_len" ]; do
|
||||
_str_c="$(printf "%s" "$_str" | cut -c "$_u_i")"
|
||||
case $_str_c in [a-zA-Z0-9.~_-])
|
||||
printf "%s" "$_str_c"
|
||||
;;
|
||||
*)
|
||||
printf "%%%02X" "'$_str_c"
|
||||
;;
|
||||
esac
|
||||
_u_i="$(_math "$_u_i" + 1)"
|
||||
done
|
||||
}
|
||||
|
||||
_ali_nonce() {
|
||||
#_head_n 1 </dev/urandom | _digest "sha256" hex | cut -c 1-31
|
||||
#Not so good...
|
||||
date +"%s%N" | sed 's/%N//g'
|
||||
}
|
||||
|
||||
_timestamp() {
|
||||
date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ"
|
||||
}
|
||||
|
||||
# stdin stdout
|
||||
_url_encode_upper() {
|
||||
encoded=$(_url_encode)
|
||||
|
||||
for match in $(echo "$encoded" | _egrep_o '%..' | sort -u); do
|
||||
upper=$(echo "$match" | _upper_case)
|
||||
encoded=$(echo "$encoded" | sed "s/$match/$upper/g")
|
||||
done
|
||||
|
||||
echo "$encoded"
|
||||
}
|
||||
|
||||
# domain pub pri
|
||||
_set_cdn_domain_ssl_certificate_query() {
|
||||
endpoint=$Ali_CDN_API
|
||||
query=''
|
||||
query=$query'AccessKeyId='$Ali_Key
|
||||
query=$query'&Action=SetCdnDomainSSLCertificate'
|
||||
|
|
@ -83,6 +152,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='$(_ali_timestamp)
|
||||
query=$query'&Timestamp='$(_timestamp)
|
||||
query=$query'&Version=2018-05-10'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034,SC2154
|
||||
|
||||
# Script to create certificate to Alibaba Cloud DCDN
|
||||
#
|
||||
# Docs: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#33-deploy-your-certificate-to-cdn-or-dcdn-of-alibaba-cloud-aliyun
|
||||
#
|
||||
# This deployment required following variables
|
||||
# export Ali_Key="ALIACCESSKEY"
|
||||
# export Ali_Secret="ALISECRETKEY"
|
||||
# The credentials are shared with all the Alibaba Cloud deploy hooks and dnsapi
|
||||
#
|
||||
# To specify the DCDN domain that is different from the certificate CN, usually used for multi-domain or wildcard certificates
|
||||
# export DEPLOY_ALI_DCDN_DOMAIN="dcdn.example.com"
|
||||
# If you have multiple CDN domains using the same certificate, just
|
||||
# export DEPLOY_ALI_DCDN_DOMAIN="dcdn1.example.com dcdn2.example.com"
|
||||
#
|
||||
# For regular CDN, see ali_cdn deploy hook
|
||||
|
||||
Ali_DCDN_API="https://dcdn.aliyuncs.com/"
|
||||
|
||||
ali_dcdn_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"
|
||||
|
||||
# Load dnsapi/dns_ali.sh to reduce the duplicated codes
|
||||
# https://github.com/acmesh-official/acme.sh/pull/5205#issuecomment-2357867276
|
||||
dnsapi_ali="$(_findHook "$_cdomain" "$_SUB_FOLDER_DNSAPI" dns_ali)"
|
||||
# shellcheck source=/dev/null
|
||||
if ! . "$dnsapi_ali"; then
|
||||
_err "Error loading file $dnsapi_ali. Please check your API file and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_prepare_ali_credentials || return 1
|
||||
|
||||
_getdeployconf DEPLOY_ALI_DCDN_DOMAIN
|
||||
if [ "$DEPLOY_ALI_DCDN_DOMAIN" ]; then
|
||||
_savedeployconf DEPLOY_ALI_DCDN_DOMAIN "$DEPLOY_ALI_DCDN_DOMAIN"
|
||||
else
|
||||
DEPLOY_ALI_DCDN_DOMAIN="$_cdomain"
|
||||
fi
|
||||
|
||||
# read cert and key files and urlencode both
|
||||
_cert=$(_url_encode upper-hex <"$_cfullchain")
|
||||
_key=$(_url_encode upper-hex <"$_ckey")
|
||||
|
||||
_debug2 _cert "$_cert"
|
||||
_debug2 _key "$_key"
|
||||
|
||||
## update domain ssl config
|
||||
for domain in $DEPLOY_ALI_DCDN_DOMAIN; do
|
||||
_set_dcdn_domain_ssl_certificate_query "$domain" "$_cert" "$_key"
|
||||
if _ali_rest "Set DCDN domain SSL certificate for $domain" "" POST; then
|
||||
_info "Domain $domain certificate has been deployed successfully"
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# domain pub pri
|
||||
_set_dcdn_domain_ssl_certificate_query() {
|
||||
endpoint=$Ali_DCDN_API
|
||||
query=''
|
||||
query=$query'AccessKeyId='$Ali_Key
|
||||
query=$query'&Action=SetDcdnDomainSSLCertificate'
|
||||
query=$query'&CertType=upload'
|
||||
query=$query'&DomainName='$1
|
||||
query=$query'&Format=json'
|
||||
query=$query'&SSLPri='$3
|
||||
query=$query'&SSLProtocol=on'
|
||||
query=$query'&SSLPub='$2
|
||||
query=$query'&SignatureMethod=HMAC-SHA1'
|
||||
query=$query"&SignatureNonce=$(_ali_nonce)"
|
||||
query=$query'&SignatureVersion=1.0'
|
||||
query=$query'&Timestamp='$(_ali_timestamp)
|
||||
query=$query'&Version=2018-01-15'
|
||||
}
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
#!/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
|
||||
}
|
||||
|
|
@ -1,394 +0,0 @@
|
|||
#!/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=$(_strip_blank_lines <"$_cfullchain" | tr -d '\r')
|
||||
_private_key=$(_strip_blank_lines <"$_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
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Script to deploy certificate to CacheFly
|
||||
# https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post
|
||||
|
||||
# This deployment required following variables
|
||||
# export CACHEFLY_TOKEN="Your CacheFly API Token"
|
||||
|
||||
# returns 0 means success, otherwise error.
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
CACHEFLY_API_BASE="https://api.cachefly.com/api/2.5"
|
||||
|
||||
cachefly_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 [ -z "$CACHEFLY_TOKEN" ]; then
|
||||
_err "CACHEFLY_TOKEN is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf CACHEFLY_TOKEN "$CACHEFLY_TOKEN"
|
||||
fi
|
||||
|
||||
_info "Deploying certificate to CacheFly..."
|
||||
|
||||
## upload certificate
|
||||
string_fullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')
|
||||
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
|
||||
|
||||
_request_body="{\"certificate\":\"$string_fullchain\",\"certificateKey\":\"$string_key\"}"
|
||||
_debug _request_body "$_request_body"
|
||||
_debug CACHEFLY_TOKEN "$CACHEFLY_TOKEN"
|
||||
export _H1="Authorization: Bearer $CACHEFLY_TOKEN"
|
||||
_response=$(_post "$_request_body" "$CACHEFLY_API_BASE/certificates" "" "POST" "application/json")
|
||||
|
||||
if _contains "$_response" "message"; then
|
||||
_err "Error in deploying $_cdomain certificate to CacheFly."
|
||||
_err "$_response"
|
||||
return 1
|
||||
fi
|
||||
_debug response "$_response"
|
||||
_info "Domain $_cdomain certificate successfully deployed to CacheFly."
|
||||
return 0
|
||||
}
|
||||
|
|
@ -52,15 +52,7 @@ cpanel_uapi_deploy() {
|
|||
|
||||
# read cert and key files and urlencode both
|
||||
_cert=$(_url_encode <"$_ccert")
|
||||
# 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
|
||||
_key=$(_url_encode <"$_ckey")
|
||||
|
||||
_debug2 _cert "$_cert"
|
||||
_debug2 _key "$_key"
|
||||
|
|
@ -87,11 +79,7 @@ cpanel_uapi_deploy() {
|
|||
# Auto mode
|
||||
if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then
|
||||
# call API for site config
|
||||
if [ -n "$_uapi_user" ]; then
|
||||
_response=$(uapi --user="$_uapi_user" DomainInfo list_domains)
|
||||
else
|
||||
_response=$(uapi DomainInfo list_domains)
|
||||
fi
|
||||
_response=$(uapi DomainInfo list_domains)
|
||||
# exit if error in response
|
||||
if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
|
||||
_err "Error in deploying certificate - cannot retrieve sitelist:"
|
||||
|
|
@ -206,8 +194,7 @@ __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 -e 's/^"//' -e 's/"$//' # YAML double-quotes values starting with '*' (wildcard subdomains)
|
||||
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p'
|
||||
}
|
||||
|
||||
# Load parameter by prefix+name - fallback to default if not set, and save to config
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Script to deploy certificate to DirectAdmin
|
||||
# https://docs.directadmin.com/directadmin/customizing-workflow/api-all-about.html#creating-a-login-key
|
||||
# https://docs.directadmin.com/changelog/version-1.24.4.html#cmd-api-catch-all-pop-passwords-frontpage-protected-dirs-ssl-certs
|
||||
|
||||
# This deployment required following variables
|
||||
# export DirectAdmin_SCHEME="https" # Optional, https or http, defaults to https
|
||||
# export DirectAdmin_ENDPOINT="example.com:2222"
|
||||
# export DirectAdmin_USERNAME="Your DirectAdmin Username"
|
||||
# export DirectAdmin_KEY="Your DirectAdmin Login Key or Password"
|
||||
# export DirectAdmin_MAIN_DOMAIN="Your DirectAdmin Main Domain, NOT Subdomain"
|
||||
|
||||
# returns 0 means success, otherwise error.
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
directadmin_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 [ -z "$DirectAdmin_ENDPOINT" ]; then
|
||||
_err "DirectAdmin_ENDPOINT is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf DirectAdmin_ENDPOINT "$DirectAdmin_ENDPOINT"
|
||||
fi
|
||||
if [ -z "$DirectAdmin_USERNAME" ]; then
|
||||
_err "DirectAdmin_USERNAME is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf DirectAdmin_USERNAME "$DirectAdmin_USERNAME"
|
||||
fi
|
||||
if [ -z "$DirectAdmin_KEY" ]; then
|
||||
_err "DirectAdmin_KEY is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf DirectAdmin_KEY "$DirectAdmin_KEY"
|
||||
fi
|
||||
if [ -z "$DirectAdmin_MAIN_DOMAIN" ]; then
|
||||
_err "DirectAdmin_MAIN_DOMAIN is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN"
|
||||
fi
|
||||
|
||||
# Optional SCHEME
|
||||
_getdeployconf DirectAdmin_SCHEME
|
||||
# set default values for DirectAdmin_SCHEME
|
||||
[ -n "${DirectAdmin_SCHEME}" ] || DirectAdmin_SCHEME="https"
|
||||
|
||||
_info "Deploying certificate to DirectAdmin..."
|
||||
|
||||
# upload certificate
|
||||
string_cfullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')
|
||||
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
|
||||
|
||||
_request_body="{\"domain\":\"$DirectAdmin_MAIN_DOMAIN\",\"action\":\"save\",\"type\":\"paste\",\"certificate\":\"$string_key\n$string_cfullchain\n\"}"
|
||||
_debug _request_body "$_request_body"
|
||||
_debug DirectAdmin_ENDPOINT "$DirectAdmin_ENDPOINT"
|
||||
_debug DirectAdmin_USERNAME "$DirectAdmin_USERNAME"
|
||||
_debug DirectAdmin_KEY "$DirectAdmin_KEY"
|
||||
_debug DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN"
|
||||
_response=$(_post "$_request_body" "$DirectAdmin_SCHEME://$DirectAdmin_USERNAME:$DirectAdmin_KEY@$DirectAdmin_ENDPOINT/CMD_API_SSL" "" "POST" "application/json")
|
||||
|
||||
if _contains "$_response" "error=1"; then
|
||||
_err "Error in deploying $_cdomain certificate to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN."
|
||||
_err "$_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "$_response"
|
||||
_info "Domain $_cdomain certificate successfully deployed to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN."
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
@ -3,8 +3,6 @@
|
|||
#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"
|
||||
|
|
@ -20,7 +18,6 @@ docker_deploy() {
|
|||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
_cpfx="$6"
|
||||
_debug _cdomain "$_cdomain"
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_LABEL
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_LABEL "$DEPLOY_DOCKER_CONTAINER_LABEL"
|
||||
|
|
@ -73,18 +70,6 @@ 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
|
||||
|
|
@ -103,12 +88,6 @@ docker_deploy() {
|
|||
_savedeployconf DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_PFX_FILE
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_PFX_FILE "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" ]; then
|
||||
_savedeployconf DEPLOY_DOCKER_CONTAINER_PFX_FILE "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then
|
||||
|
|
@ -126,20 +105,6 @@ 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
|
||||
|
|
@ -160,12 +125,6 @@ docker_deploy() {
|
|||
fi
|
||||
fi
|
||||
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" ]; then
|
||||
if ! _docker_cp "$_cid" "$_cpfx" "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then
|
||||
_info "Reloading: $DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
|
||||
if ! _docker_exec "$_cid" "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"; then
|
||||
|
|
@ -217,22 +176,10 @@ _docker_exec() {
|
|||
_debug2 cjson "$cjson"
|
||||
execid="$(echo "$cjson" | cut -d '"' -f 4)"
|
||||
_debug execid "$execid"
|
||||
#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}")"
|
||||
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": false,\"Tty\": false}")"
|
||||
_debug2 ejson "$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"
|
||||
if [ "$ejson" ]; then
|
||||
_err "$ejson"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Here is a script to deploy cert to edgio using its API
|
||||
# https://docs.edg.io/guides/v7/develop/rest_api/authentication
|
||||
# https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts
|
||||
|
||||
# This deployment required following variables
|
||||
# export EDGIO_CLIENT_ID="Your Edgio Client ID"
|
||||
# export EDGIO_CLIENT_SECRET="Your Edgio Client Secret"
|
||||
# export EDGIO_ENVIRONMENT_ID="Your Edgio Environment ID"
|
||||
|
||||
# If have more than one Environment ID
|
||||
# export EDGIO_ENVIRONMENT_ID="ENVIRONMENT_ID_1 ENVIRONMENT_ID_2"
|
||||
|
||||
# returns 0 means success, otherwise error.
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
edgio_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 [ -z "$EDGIO_CLIENT_ID" ]; then
|
||||
_err "EDGIO_CLIENT_ID is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf EDGIO_CLIENT_ID "$EDGIO_CLIENT_ID"
|
||||
fi
|
||||
|
||||
if [ -z "$EDGIO_CLIENT_SECRET" ]; then
|
||||
_err "EDGIO_CLIENT_SECRET is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf EDGIO_CLIENT_SECRET "$EDGIO_CLIENT_SECRET"
|
||||
fi
|
||||
|
||||
if [ -z "$EDGIO_ENVIRONMENT_ID" ]; then
|
||||
_err "EDGIO_ENVIRONMENT_ID is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf EDGIO_ENVIRONMENT_ID "$EDGIO_ENVIRONMENT_ID"
|
||||
fi
|
||||
|
||||
_info "Getting access token"
|
||||
_data="client_id=$EDGIO_CLIENT_ID&client_secret=$EDGIO_CLIENT_SECRET&grant_type=client_credentials&scope=app.config"
|
||||
_debug Get_access_token_data "$_data"
|
||||
_response=$(_post "$_data" "https://id.edgio.app/connect/token" "" "POST" "application/x-www-form-urlencoded")
|
||||
_debug Get_access_token_response "$_response"
|
||||
_access_token=$(echo "$_response" | _json_decode | _egrep_o '"access_token":"[^"]*' | cut -d : -f 2 | tr -d '"')
|
||||
_debug _access_token "$_access_token"
|
||||
if [ -z "$_access_token" ]; then
|
||||
_err "Error in getting access token"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Uploading certificate"
|
||||
string_ccert=$(sed 's/$/\\n/' "$_ccert" | tr -d '\n')
|
||||
string_cca=$(sed 's/$/\\n/' "$_cca" | tr -d '\n')
|
||||
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
|
||||
|
||||
for ENVIRONMENT_ID in $EDGIO_ENVIRONMENT_ID; do
|
||||
_data="{\"environment_id\":\"$ENVIRONMENT_ID\",\"primary_cert\":\"$string_ccert\",\"intermediate_cert\":\"$string_cca\",\"private_key\":\"$string_key\"}"
|
||||
_debug Upload_certificate_data "$_data"
|
||||
_H1="Authorization: Bearer $_access_token"
|
||||
_response=$(_post "$_data" "https://edgioapis.com/config/v0.1/tls-certs" "" "POST" "application/json")
|
||||
if _contains "$_response" "message"; then
|
||||
_err "Error in deploying $_cdomain certificate to Edgio ENVIRONMENT_ID $ENVIRONMENT_ID."
|
||||
_err "$_response"
|
||||
return 1
|
||||
fi
|
||||
_debug Upload_certificate_response "$_response"
|
||||
_info "Domain $_cdomain certificate successfully deployed to Edgio ENVIRONMENT_ID $ENVIRONMENT_ID."
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
@ -109,5 +109,6 @@ exim4_deploy() {
|
|||
fi
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
#!/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
|
||||
}
|
||||
|
|
@ -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/iam/auth/jwt/login")
|
||||
_response=$(_post "$_request" "https://api.gcore.com/auth/jwt/login")
|
||||
_debug _response "$_response"
|
||||
_regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$"
|
||||
_debug _regex "$_regex"
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@
|
|||
# needing to reload HAProxy. Default is "no".
|
||||
#
|
||||
# Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat
|
||||
# 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")
|
||||
# address format.
|
||||
#
|
||||
# export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock"
|
||||
#
|
||||
|
|
@ -194,6 +193,7 @@ 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,18 +272,12 @@ haproxy_deploy() {
|
|||
_cafile_argument=""
|
||||
fi
|
||||
_debug _cafile_argument "${_cafile_argument}"
|
||||
# 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)
|
||||
# if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed
|
||||
_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_name}" = "LibreSSL" ]; then
|
||||
_header_sep=" "
|
||||
elif [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
|
||||
if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
|
||||
_header_sep="="
|
||||
else
|
||||
_header_sep=" "
|
||||
|
|
@ -333,67 +327,62 @@ haproxy_deploy() {
|
|||
|
||||
# Update certificate over HAProxy stats socket or master CLI.
|
||||
if _exists socat; then
|
||||
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}"
|
||||
# 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}"
|
||||
_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}"
|
||||
_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}."
|
||||
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
|
||||
return "${_ret}"
|
||||
fi
|
||||
# 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}"
|
||||
# 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'"
|
||||
_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}"
|
||||
_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}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't update '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
_err "'socat' is not available, couldn't update over ${_socketname}"
|
||||
fi
|
||||
|
|
|
|||
114
deploy/ikuai.sh
114
deploy/ikuai.sh
|
|
@ -1,114 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Here is a script to deploy cert to ikuai using curl
|
||||
#
|
||||
# it requires following environment variables:
|
||||
#
|
||||
# IKUAI_SCHEME="http" - http or https , defaults to "http"
|
||||
# IKUAI_HOSTNAME="localhost" - host , defaults to "192.168.9.1"
|
||||
# IKUAI_PORT="80" - port , defaults to "80"
|
||||
# IKUAI_USERNAME="admin" - username , defaults to "admin"
|
||||
# IKUAI_PASSWORD="yourPassword" - password
|
||||
# IKUAI_CERT_ID=1 - ikuai cert id , defaults to 1, and only 1 is supported for now !!!
|
||||
#
|
||||
#returns 0 means success, otherwise error.
|
||||
#
|
||||
######## Public functions #####################
|
||||
#
|
||||
#domain keyfile certfile cafile fullchain
|
||||
ikuai_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"
|
||||
|
||||
# Get deploy conf
|
||||
_getdeployconf IKUAI_SCHEME
|
||||
_getdeployconf IKUAI_HOSTNAME
|
||||
_getdeployconf IKUAI_PORT
|
||||
_getdeployconf IKUAI_USERNAME
|
||||
_getdeployconf IKUAI_PASSWORD
|
||||
_getdeployconf IKUAI_CERT_ID
|
||||
|
||||
# Use default if not provided
|
||||
[ -n "$IKUAI_SCHEME" ] || IKUAI_SCHEME="http"
|
||||
[ -n "$IKUAI_HOSTNAME" ] || IKUAI_HOSTNAME="192.168.9.1"
|
||||
[ -n "$IKUAI_PORT" ] || IKUAI_PORT=80
|
||||
[ -n "$IKUAI_USERNAME" ] || IKUAI_USERNAME="admin"
|
||||
[ -n "$IKUAI_CERT_ID" ] || IKUAI_CERT_ID=1
|
||||
|
||||
if [ -z "$IKUAI_PASSWORD" ]; then
|
||||
_err "please define IKUAI_PASSWORD."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 IKUAI_SCHEME "$IKUAI_SCHEME"
|
||||
_debug2 IKUAI_HOSTNAME "$IKUAI_HOSTNAME"
|
||||
_debug2 IKUAI_PORT "$IKUAI_PORT"
|
||||
_debug2 IKUAI_USERNAME "$IKUAI_USERNAME"
|
||||
_secure_debug2 IKUAI_PASSWORD "$IKUAI_PASSWORD"
|
||||
|
||||
_info "Login to ikuai ..."
|
||||
_ikuai_url="$IKUAI_SCHEME://$IKUAI_HOSTNAME:$IKUAI_PORT"
|
||||
_pass_md5="$(printf "%s" "$IKUAI_PASSWORD" | _digest md5 hex | _lower_case)"
|
||||
_pass_salt="$(printf "salt_11%s" "$IKUAI_PASSWORD" | _base64)"
|
||||
_debug2 _ikuai_url "$_ikuai_url"
|
||||
|
||||
_login_req="{\"username\":\"$IKUAI_USERNAME\",\"passwd\":\"$_pass_md5\",\"pass\":\"$_pass_salt\",\"remember_password\":\"\"}"
|
||||
_response=$(_post "$_login_req" "$_ikuai_url/Action/login" "" "POST" "application/json")
|
||||
|
||||
_err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)"
|
||||
# check ErrMsg
|
||||
if [ "$_err_msg" != "Success" ]; then
|
||||
_err "Failed to login to ikuai: $_err_msg"
|
||||
return 1
|
||||
fi
|
||||
# check cookie
|
||||
_cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | sed 's/;.*//')"
|
||||
if [ -z "$_cookie" ]; then
|
||||
_err "Fail to get the cookie."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Set cookie header
|
||||
_H1="Cookie: $_cookie; username=$IKUAI_USERNAME; login=1"
|
||||
|
||||
_info "Deploy the cert to ikuai ... "
|
||||
|
||||
# Should replace \n to @ ," " to #
|
||||
_cert_content_single_line="$(tr <"$_cfullchain" '\n' '@' | tr ' ' '#')"
|
||||
_key_content_single_line="$(tr <"$_ckey" '\n' '@' | tr ' ' '#')"
|
||||
|
||||
_debug2 _cert_content_single_line "$_cert_content_single_line"
|
||||
_secure_debug2 _key_content_single_line "$_key_content_single_line"
|
||||
|
||||
_key_manager_req="{\"func_name\":\"key_manager\",\"action\":\"save\",\"param\":{\"ca\":\"$_cert_content_single_line\",\"key\":\"$_key_content_single_line\",\"id\":$IKUAI_CERT_ID,\"enabled\":\"yes\",\"comment\":\"\"}}"
|
||||
_response=$(_post "$_key_manager_req" "$_ikuai_url/Action/call" "" "POST" "application/json")
|
||||
|
||||
_err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)"
|
||||
# check ErrMsg
|
||||
if [ "$_err_msg" != "Success" ]; then
|
||||
_err "Failed to deploy the cert to ikuai: $_err_msg"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Save the deploy config ... "
|
||||
# Save the config
|
||||
_savedeployconf IKUAI_SCHEME "$IKUAI_SCHEME"
|
||||
_savedeployconf IKUAI_HOSTNAME "$IKUAI_HOSTNAME"
|
||||
_savedeployconf IKUAI_PORT "$IKUAI_PORT"
|
||||
_savedeployconf IKUAI_USERNAME "$IKUAI_USERNAME"
|
||||
_savedeployconf IKUAI_PASSWORD "$IKUAI_PASSWORD"
|
||||
_savedeployconf IKUAI_CERT_ID "$IKUAI_CERT_ID"
|
||||
|
||||
_info "Successfully deployed certificate to ikuai. Enjoy! :>"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
#Here is a script to deploy cert to a Kemp Loadmaster.
|
||||
|
||||
#returns 0 means success, otherwise error.
|
||||
|
||||
#DEPLOY_KEMP_TOKEN="token"
|
||||
#DEPLOY_KEMP_URL="https://kemplm.example.com"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
kemplm_deploy() {
|
||||
_domain="$1"
|
||||
_key_file="$2"
|
||||
_cert_file="$3"
|
||||
_ca_file="$4"
|
||||
_fullchain_file="$5"
|
||||
|
||||
_debug _domain "$_domain"
|
||||
_debug _key_file "$_key_file"
|
||||
_debug _cert_file "$_cert_file"
|
||||
_debug _ca_file "$_ca_file"
|
||||
_debug _fullchain_file "$_fullchain_file"
|
||||
|
||||
if ! _exists jq; then
|
||||
_err "jq not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Rename wildcard certs, kemp accepts only alphanumeric names so we delete '*.' from filename
|
||||
_kemp_domain=$(echo "${_domain}" | sed 's/\*\.//')
|
||||
_debug _kemp_domain "$_kemp_domain"
|
||||
|
||||
# Read config from saved values or env
|
||||
_getdeployconf DEPLOY_KEMP_TOKEN
|
||||
_getdeployconf DEPLOY_KEMP_URL
|
||||
|
||||
_debug DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL"
|
||||
_secure_debug DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN"
|
||||
|
||||
if [ -z "$DEPLOY_KEMP_TOKEN" ]; then
|
||||
_err "Kemp Loadmaster token is not found, please define DEPLOY_KEMP_TOKEN."
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$DEPLOY_KEMP_URL" ]; then
|
||||
_err "Kemp Loadmaster URL is not found, please define DEPLOY_KEMP_URL."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Save current values
|
||||
_savedeployconf DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN"
|
||||
_savedeployconf DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL"
|
||||
|
||||
# Check if certificate is already installed
|
||||
_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}$")
|
||||
_debug2 _kemp_cert_count "${_kemp_cert_count}"
|
||||
|
||||
_kemp_replace_cert=1
|
||||
if [ "${_kemp_cert_count}" -eq 0 ]; then
|
||||
_kemp_replace_cert=0
|
||||
_info "Certificate does not exist on Kemp Loadmaster"
|
||||
else
|
||||
_info "Certificate already exists on Kemp Loadmaster"
|
||||
fi
|
||||
_debug _kemp_replace_cert "${_kemp_replace_cert}"
|
||||
|
||||
# Upload new certificate to Kemp Loadmaster
|
||||
_kemp_upload_cert=$(_mktemp)
|
||||
cat "${_fullchain_file}" "${_key_file}" | base64 | tr -d '\n' >"${_kemp_upload_cert}"
|
||||
|
||||
_info "Uploading certificate to Kemp Loadmaster"
|
||||
_add_data=$(cat "${_kemp_upload_cert}")
|
||||
_add_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"${_add_data}\"}"
|
||||
_debug3 _add_request "${_add_request}"
|
||||
_kemp_post_result=$(HTTPS_INSECURE=1 _post "${_add_request}" "${DEPLOY_KEMP_URL}/accessv2")
|
||||
_retval=$?
|
||||
_debug2 _kemp_post_result "${_kemp_post_result}"
|
||||
if [ "${_retval}" -eq 0 ]; then
|
||||
_kemp_post_status=$(echo "${_kemp_post_result}" | jq -r '.status')
|
||||
_kemp_post_message=$(echo "${_kemp_post_result}" | jq -r '.message')
|
||||
if [ "${_kemp_post_status}" = "ok" ]; then
|
||||
_info "Upload successful"
|
||||
else
|
||||
_err "Upload failed: ${_kemp_post_message}"
|
||||
_retval=1
|
||||
fi
|
||||
else
|
||||
_err "Upload failed"
|
||||
_retval=1
|
||||
fi
|
||||
|
||||
rm "${_kemp_upload_cert}"
|
||||
|
||||
return $_retval
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Script to deploy certificate to KeyHelp
|
||||
# This deployment required following variables
|
||||
# export DEPLOY_KEYHELP_BASEURL="https://keyhelp.example.com"
|
||||
# export DEPLOY_KEYHELP_USERNAME="Your KeyHelp Username"
|
||||
# export DEPLOY_KEYHELP_PASSWORD="Your KeyHelp Password"
|
||||
# export DEPLOY_KEYHELP_DOMAIN_ID="Depoly certificate to this Domain ID"
|
||||
|
||||
# Open the 'Edit domain' page, and you will see id=xxx at the end of the URL. This is the Domain ID.
|
||||
# https://DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit&id=xxx
|
||||
|
||||
# If have more than one domain name
|
||||
# export DEPLOY_KEYHELP_DOMAIN_ID="111 222 333"
|
||||
|
||||
keyhelp_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 [ -z "$DEPLOY_KEYHELP_BASEURL" ]; then
|
||||
_err "DEPLOY_KEYHELP_BASEURL is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf DEPLOY_KEYHELP_BASEURL "$DEPLOY_KEYHELP_BASEURL"
|
||||
fi
|
||||
|
||||
if [ -z "$DEPLOY_KEYHELP_USERNAME" ]; then
|
||||
_err "DEPLOY_KEYHELP_USERNAME is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf DEPLOY_KEYHELP_USERNAME "$DEPLOY_KEYHELP_USERNAME"
|
||||
fi
|
||||
|
||||
if [ -z "$DEPLOY_KEYHELP_PASSWORD" ]; then
|
||||
_err "DEPLOY_KEYHELP_PASSWORD is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf DEPLOY_KEYHELP_PASSWORD "$DEPLOY_KEYHELP_PASSWORD"
|
||||
fi
|
||||
|
||||
if [ -z "$DEPLOY_KEYHELP_DOMAIN_ID" ]; then
|
||||
_err "DEPLOY_KEYHELP_DOMAIN_ID is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf DEPLOY_KEYHELP_DOMAIN_ID "$DEPLOY_KEYHELP_DOMAIN_ID"
|
||||
fi
|
||||
|
||||
# Optional DEPLOY_KEYHELP_ENFORCE_HTTPS
|
||||
_getdeployconf DEPLOY_KEYHELP_ENFORCE_HTTPS
|
||||
# set default values for DEPLOY_KEYHELP_ENFORCE_HTTPS
|
||||
[ -n "${DEPLOY_KEYHELP_ENFORCE_HTTPS}" ] || DEPLOY_KEYHELP_ENFORCE_HTTPS="1"
|
||||
|
||||
_info "Logging in to keyhelp panel"
|
||||
username_encoded="$(printf "%s" "${DEPLOY_KEYHELP_USERNAME}" | _url_encode)"
|
||||
password_encoded="$(printf "%s" "${DEPLOY_KEYHELP_PASSWORD}" | _url_encode)"
|
||||
_H1="Content-Type: application/x-www-form-urlencoded"
|
||||
_response=$(_get "$DEPLOY_KEYHELP_BASEURL/index.php?submit=1&username=$username_encoded&password=$password_encoded" "TRUE")
|
||||
_cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)"
|
||||
|
||||
# If cookies is not empty then logon successful
|
||||
if [ -z "$_cookie" ]; then
|
||||
_err "Fail to get cookie."
|
||||
return 1
|
||||
fi
|
||||
_debug "cookie" "$_cookie"
|
||||
|
||||
_info "Uploading certificate"
|
||||
_date=$(date +"%Y%m%d")
|
||||
encoded_key="$(_url_encode <"$_ckey")"
|
||||
encoded_ccert="$(_url_encode <"$_ccert")"
|
||||
encoded_cca="$(_url_encode <"$_cca")"
|
||||
certificate_name="$_cdomain-$_date"
|
||||
|
||||
_request_body="submit=1&certificate_name=$certificate_name&add_type=upload&text_private_key=$encoded_key&text_certificate=$encoded_ccert&text_ca_certificate=$encoded_cca"
|
||||
_H1="Cookie: $_cookie"
|
||||
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST")
|
||||
_message=$(echo "$_response" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
|
||||
_info "_message" "$_message"
|
||||
if [ -z "$_message" ]; then
|
||||
_err "Fail to upload certificate."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for DOMAIN_ID in $DEPLOY_KEYHELP_DOMAIN_ID; do
|
||||
_info "Apply certificate to domain id $DOMAIN_ID"
|
||||
_response=$(_get "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit&id=$DOMAIN_ID")
|
||||
cert_value=$(echo "$_response" | grep "$certificate_name" | sed -n 's/.*value="\([^"]*\).*/\1/p')
|
||||
target_type=$(echo "$_response" | grep 'target_type' | grep 'checked' | sed -n 's/.*value="\([^"]*\).*/\1/p')
|
||||
if [ "$target_type" = "directory" ]; then
|
||||
path=$(echo "$_response" | awk '/name="path"/{getline; print}' | sed -n 's/.*value="\([^"]*\).*/\1/p')
|
||||
fi
|
||||
echo "$_response" | grep "is_prefer_https" | grep "checked" >/dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
is_prefer_https=1
|
||||
else
|
||||
is_prefer_https=0
|
||||
fi
|
||||
echo "$_response" | grep "hsts_enabled" | grep "checked" >/dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
hsts_enabled=1
|
||||
else
|
||||
hsts_enabled=0
|
||||
fi
|
||||
_debug "cert_value" "$cert_value"
|
||||
if [ -z "$cert_value" ]; then
|
||||
_err "Fail to get certificate id."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&path=$path&is_prefer_https=$is_prefer_https&hsts_enabled=$hsts_enabled&certificate_type=custom&certificate_id=$cert_value&enforce_https=$DEPLOY_KEYHELP_ENFORCE_HTTPS"
|
||||
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit" "" "POST")
|
||||
_message=$(echo "$_response" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
|
||||
_info "_message" "$_message"
|
||||
if [ -z "$_message" ]; then
|
||||
_err "Fail to apply certificate."
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
_info "Domain $_cdomain certificate successfully deployed to KeyHelp Domain ID $DEPLOY_KEYHELP_DOMAIN_ID."
|
||||
return 0
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
keyhelp_api_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
|
||||
# Read config from saved values or env
|
||||
_getdeployconf DEPLOY_KEYHELP_HOST
|
||||
_getdeployconf DEPLOY_KEYHELP_API_KEY
|
||||
|
||||
_debug DEPLOY_KEYHELP_HOST "$DEPLOY_KEYHELP_HOST"
|
||||
_secure_debug DEPLOY_KEYHELP_API_KEY "$DEPLOY_KEYHELP_API_KEY"
|
||||
|
||||
if [ -z "$DEPLOY_KEYHELP_HOST" ]; then
|
||||
_err "KeyHelp host not found, please define DEPLOY_KEYHELP_HOST."
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$DEPLOY_KEYHELP_API_KEY" ]; then
|
||||
_err "KeyHelp api key not found, please define DEPLOY_KEYHELP_API_KEY."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Save current values
|
||||
_savedeployconf DEPLOY_KEYHELP_HOST "$DEPLOY_KEYHELP_HOST"
|
||||
_savedeployconf DEPLOY_KEYHELP_API_KEY "$DEPLOY_KEYHELP_API_KEY"
|
||||
|
||||
_request_key="$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')"
|
||||
_request_cert="$(tr '\n' ':' <"$_ccert" | sed 's/:/\\n/g')"
|
||||
_request_ca="$(tr '\n' ':' <"$_cca" | sed 's/:/\\n/g')"
|
||||
|
||||
_request_body="{
|
||||
\"name\": \"$_cdomain\",
|
||||
\"components\": {
|
||||
\"private_key\": \"$_request_key\",
|
||||
\"certificate\": \"$_request_cert\",
|
||||
\"ca_certificate\": \"$_request_ca\"
|
||||
}
|
||||
}"
|
||||
|
||||
_hosts="$(echo "$DEPLOY_KEYHELP_HOST" | tr "," " ")"
|
||||
_keys="$(echo "$DEPLOY_KEYHELP_API_KEY" | tr "," " ")"
|
||||
_i=1
|
||||
|
||||
for _host in $_hosts; do
|
||||
_key="$(_getfield "$_keys" "$_i" " ")"
|
||||
_i="$(_math "$_i" + 1)"
|
||||
|
||||
export _H1="X-API-Key: $_key"
|
||||
|
||||
_put_url="$_host/api/v2/certificates/name/$_cdomain"
|
||||
if _post "$_request_body" "$_put_url" "" "PUT" "application/json" >/dev/null; then
|
||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||
else
|
||||
_err "Cannot make PUT request to $_put_url"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_code" = "404" ]; then
|
||||
_info "$_cdomain not found, creating new entry at $_host"
|
||||
|
||||
_post_url="$_host/api/v2/certificates"
|
||||
if _post "$_request_body" "$_post_url" "" "POST" "application/json" >/dev/null; then
|
||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||
else
|
||||
_err "Cannot make POST request to $_post_url"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if _startswith "$_code" "2"; then
|
||||
_info "$_cdomain set at $_host"
|
||||
else
|
||||
_err "HTTP status code is $_code"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
#!/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
|
||||
}
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
#!/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"
|
||||
# A name without a leading '/' is looked up in the certificate directory
|
||||
# of the domain. An absolute path is used as is, so a single deploy file
|
||||
# can be shared by all domains, e.g.
|
||||
# - export MULTIDEPLOY_FILENAME="/etc/acme/multideploy.yml"
|
||||
#
|
||||
# 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"
|
||||
|
||||
_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. A name starting with '/' is treated as an
|
||||
# absolute path, any other name is relative to the domain directory.
|
||||
# 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
|
||||
found_file=""
|
||||
for file in "$@"; do
|
||||
if _startswith "$file" "/"; then
|
||||
_multideploy_path="$file"
|
||||
else
|
||||
_multideploy_path="$DOMAIN_PATH/$file"
|
||||
fi
|
||||
_debug3 "Checking file" "$_multideploy_path"
|
||||
if [ -f "$_multideploy_path" ]; 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="$_multideploy_path"
|
||||
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 "$found_file"; then
|
||||
_err "Deploy file is not valid: $found_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$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"
|
||||
}
|
||||
|
|
@ -54,8 +54,6 @@ mydevil_deploy() {
|
|||
# Usage: ip=$(mydevil_get_ip domain.com)
|
||||
# echo $ip
|
||||
mydevil_get_ip() {
|
||||
# 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
|
||||
devil dns list "$1" | cut -w -s -f 3,7 | grep "^A$(printf '\t')" | cut -w -s -f 2 || return 1
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Script to deploy certificate to Netlify
|
||||
# https://docs.netlify.com/api/get-started/#authentication
|
||||
# https://open-api.netlify.com/#tag/sniCertificate
|
||||
|
||||
# This deployment required following variables
|
||||
# export Netlify_ACCESS_TOKEN="Your Netlify Access Token"
|
||||
# export Netlify_SITE_ID="Your Netlify Site ID"
|
||||
|
||||
# If have more than one SITE ID
|
||||
# export Netlify_SITE_ID="SITE_ID_1 SITE_ID_2"
|
||||
|
||||
# returns 0 means success, otherwise error.
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
netlify_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 [ -z "$Netlify_ACCESS_TOKEN" ]; then
|
||||
_err "Netlify_ACCESS_TOKEN is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf Netlify_ACCESS_TOKEN "$Netlify_ACCESS_TOKEN"
|
||||
fi
|
||||
if [ -z "$Netlify_SITE_ID" ]; then
|
||||
_err "Netlify_SITE_ID is not defined."
|
||||
return 1
|
||||
else
|
||||
_savedomainconf Netlify_SITE_ID "$Netlify_SITE_ID"
|
||||
fi
|
||||
|
||||
_info "Deploying certificate to Netlify..."
|
||||
|
||||
## upload certificate
|
||||
string_ccert=$(sed 's/$/\\n/' "$_ccert" | tr -d '\n')
|
||||
string_cca=$(sed 's/$/\\n/' "$_cca" | tr -d '\n')
|
||||
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
|
||||
|
||||
for SITE_ID in $Netlify_SITE_ID; do
|
||||
_request_body="{\"certificate\":\"$string_ccert\",\"key\":\"$string_key\",\"ca_certificates\":\"$string_cca\"}"
|
||||
_debug _request_body "$_request_body"
|
||||
_debug Netlify_ACCESS_TOKEN "$Netlify_ACCESS_TOKEN"
|
||||
export _H1="Authorization: Bearer $Netlify_ACCESS_TOKEN"
|
||||
_response=$(_post "$_request_body" "https://api.netlify.com/api/v1/sites/$SITE_ID/ssl" "" "POST" "application/json")
|
||||
|
||||
if _contains "$_response" "\"error\""; then
|
||||
_err "Error in deploying $_cdomain certificate to Netlify SITE_ID $SITE_ID."
|
||||
_err "$_response"
|
||||
return 1
|
||||
fi
|
||||
_debug response "$_response"
|
||||
_info "Domain $_cdomain certificate successfully deployed to Netlify SITE_ID $SITE_ID."
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
151
deploy/panos.sh
151
deploy/panos.sh
|
|
@ -7,27 +7,20 @@
|
|||
#
|
||||
# Firewall admin with superuser and IP address is required.
|
||||
#
|
||||
# REQUIRED:
|
||||
# REQURED:
|
||||
# export PANOS_HOST=""
|
||||
# export PANOS_USER="" #User *MUST* have Commit and Import Permissions in XML API for Admin Role
|
||||
# export PANOS_PASS=""
|
||||
#
|
||||
# OPTIONAL
|
||||
# export PANOS_TEMPLATE="" # Template Name of panorama managed devices
|
||||
# export PANOS_TEMPLATE_STACK="" # set a Template Stack if certificate should also be pushed automatically
|
||||
# export PANOS_VSYS="Shared" # name of the vsys to import the certificate
|
||||
# export PANOS_CERTNAME="" # use a custom certificate name to work around Panorama's 31-character limit
|
||||
# export PANOS_TEMPLATE="" #Template Name of panorama managed devices
|
||||
#
|
||||
# The script will automatically generate a new API key if
|
||||
# no key is found, or if a saved key has expired or is invalid.
|
||||
|
||||
_COMMIT_WAIT_INTERVAL=30 # query commit status every 30 seconds
|
||||
_COMMIT_WAIT_ITERATIONS=20 # query commit status 20 times (20*30 = 600 seconds = 10 minutes)
|
||||
|
||||
# This function is to parse the XML response from the firewall
|
||||
parse_response() {
|
||||
type=$2
|
||||
_debug "API Response: $1"
|
||||
if [ "$type" = 'keygen' ]; then
|
||||
status=$(echo "$1" | sed 's/^.*\(['\'']\)\([a-z]*\)'\''.*/\2/g')
|
||||
if [ "$status" = "success" ]; then
|
||||
|
|
@ -37,13 +30,6 @@ parse_response() {
|
|||
message="PAN-OS Key could not be set."
|
||||
fi
|
||||
else
|
||||
if [ "$type" = 'commit' ]; then
|
||||
job_id=$(echo "$1" | sed 's/^.*\(<job>\)\(.*\)<\/job>.*/\2/g')
|
||||
_commit_job_id=$job_id
|
||||
elif [ "$type" = 'job_status' ]; then
|
||||
job_status=$(echo "$1" | tr -d '\n' | sed 's/^.*<result>\([^<]*\)<\/result>.*/\1/g')
|
||||
_commit_job_status=$job_status
|
||||
fi
|
||||
status=$(echo "$1" | tr -d '\n' | sed 's/^.*"\([a-z]*\)".*/\1/g')
|
||||
message=$(echo "$1" | tr -d '\n' | sed 's/.*\(<result>\|<msg>\|<line>\)\([^<]*\).*/\2/g')
|
||||
_debug "Firewall message: $message"
|
||||
|
|
@ -58,23 +44,24 @@ parse_response() {
|
|||
#This function is used to deploy to the firewall
|
||||
deployer() {
|
||||
content=""
|
||||
type=$1 # Types are keytest, keygen, cert, key, commit, job_status, push
|
||||
type=$1 # Types are keytest, keygen, cert, key, commit
|
||||
panos_url="https://$_panos_host/api/"
|
||||
export _H1="Content-Type: application/x-www-form-urlencoded"
|
||||
|
||||
#Test API Key by performing a lookup
|
||||
if [ "$type" = 'keytest' ]; then
|
||||
_debug "**** Testing saved API Key ****"
|
||||
_H1="Content-Type: application/x-www-form-urlencoded"
|
||||
# Get Version Info to test key
|
||||
content="type=version&key=$_panos_key"
|
||||
## Exclude all scopes for the empty 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>"
|
||||
#_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>"
|
||||
fi
|
||||
|
||||
# Generate API Key
|
||||
if [ "$type" = 'keygen' ]; then
|
||||
_debug "**** Generating new API Key ****"
|
||||
_H1="Content-Type: application/x-www-form-urlencoded"
|
||||
content="type=keygen&user=$_panos_user&password=$_panos_pass"
|
||||
# content="$content${nl}--$delim${nl}Content-Disposition: form-data; type=\"keygen\"; user=\"$_panos_user\"; password=\"$_panos_pass\"${nl}Content-Type: application/octet-stream${nl}${nl}"
|
||||
fi
|
||||
|
|
@ -90,31 +77,25 @@ deployer() {
|
|||
if [ "$type" = 'cert' ]; then
|
||||
panos_url="${panos_url}?type=import"
|
||||
content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\ncertificate"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")"
|
||||
if [ "$_panos_template" ]; then
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template"
|
||||
fi
|
||||
if [ "$_panos_vsys" ]; then
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl-vsys\"\r\n\r\n$_panos_vsys"
|
||||
fi
|
||||
fi
|
||||
if [ "$type" = 'key' ]; then
|
||||
panos_url="${panos_url}?type=import"
|
||||
content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\nprivate-key"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"passphrase\"\r\n\r\n123456"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_panos_certname.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cdomain.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
|
||||
if [ "$_panos_template" ]; then
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template"
|
||||
fi
|
||||
if [ "$_panos_vsys" ]; then
|
||||
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl-vsys\"\r\n\r\n$_panos_vsys"
|
||||
fi
|
||||
fi
|
||||
#Close multipart
|
||||
content="$content${nl}--$delim--${nl}${nl}"
|
||||
|
|
@ -125,30 +106,18 @@ deployer() {
|
|||
# Commit changes
|
||||
if [ "$type" = 'commit' ]; then
|
||||
_debug "**** Committing changes ****"
|
||||
export _H1="Content-Type: application/x-www-form-urlencoded"
|
||||
#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><force><partial><admin><member>$_panos_user</member></admin></partial></force></commit>" | _url_encode)
|
||||
cmd=$(printf "%s" "<commit><partial><force><admin><member>$_panos_user</member></admin></force></partial></commit>" | _url_encode)
|
||||
else
|
||||
cmd=$(printf "%s" "<commit><partial><admin><member>$_panos_user</member></admin></partial></commit>" | _url_encode)
|
||||
_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)
|
||||
fi
|
||||
content="type=commit&action=partial&key=$_panos_key&cmd=$cmd"
|
||||
fi
|
||||
|
||||
# Query job status
|
||||
if [ "$type" = 'job_status' ]; then
|
||||
echo "**** Querying job $_commit_job_id status ****"
|
||||
cmd=$(printf "%s" "<show><jobs><id>$_commit_job_id</id></jobs></show>" | _url_encode)
|
||||
content="type=op&key=$_panos_key&cmd=$cmd"
|
||||
fi
|
||||
|
||||
# Push changes
|
||||
if [ "$type" = 'push' ]; then
|
||||
echo "**** Pushing changes ****"
|
||||
cmd=$(printf "%s" "<commit-all><template-stack><name>$_panos_template_stack</name><admin><member>$_panos_user</member></admin></template-stack></commit-all>" | _url_encode)
|
||||
content="type=commit&action=all&key=$_panos_key&cmd=$cmd"
|
||||
fi
|
||||
|
||||
response=$(_post "$content" "$panos_url" "" "POST")
|
||||
parse_response "$response" "$type"
|
||||
# Saving response to variables
|
||||
|
|
@ -157,8 +126,6 @@ deployer() {
|
|||
if [ "$response_status" = "success" ]; then
|
||||
_debug "Successfully deployed $type"
|
||||
return 0
|
||||
elif [ "$_commit_job_status" ]; then
|
||||
_debug "Commit Job Status = $_commit_job_status"
|
||||
else
|
||||
_err "Deploy of type $type failed. Try deploying with --debug to troubleshoot."
|
||||
_debug "$message"
|
||||
|
|
@ -206,12 +173,13 @@ panos_deploy() {
|
|||
fi
|
||||
|
||||
# PANOS_KEY
|
||||
_getdeployconf PANOS_KEY
|
||||
if [ "$PANOS_KEY" ]; then
|
||||
_debug "Detected ENV variable PANOS_KEY. Saving to file."
|
||||
_savedeployconf PANOS_KEY "$PANOS_KEY" 1
|
||||
_debug "Detected saved key."
|
||||
_panos_key=$PANOS_KEY
|
||||
else
|
||||
_debug "Attempting to load variable PANOS_KEY from file."
|
||||
_getdeployconf PANOS_KEY
|
||||
_debug "No key detected"
|
||||
unset _panos_key
|
||||
fi
|
||||
|
||||
# PANOS_TEMPLATE
|
||||
|
|
@ -223,42 +191,11 @@ panos_deploy() {
|
|||
_getdeployconf PANOS_TEMPLATE
|
||||
fi
|
||||
|
||||
# PANOS_TEMPLATE_STACK
|
||||
if [ "$PANOS_TEMPLATE_STACK" ]; then
|
||||
_debug "Detected ENV variable PANOS_TEMPLATE_STACK. Saving to file."
|
||||
_savedeployconf PANOS_TEMPLATE_STACK "$PANOS_TEMPLATE_STACK" 1
|
||||
else
|
||||
_debug "Attempting to load variable PANOS_TEMPLATE_STACK from file."
|
||||
_getdeployconf PANOS_TEMPLATE_STACK
|
||||
fi
|
||||
|
||||
# PANOS_TEMPLATE_STACK
|
||||
if [ "$PANOS_VSYS" ]; then
|
||||
_debug "Detected ENV variable PANOS_VSYS. Saving to file."
|
||||
_savedeployconf PANOS_VSYS "$PANOS_VSYS" 1
|
||||
else
|
||||
_debug "Attempting to load variable PANOS_VSYS from file."
|
||||
_getdeployconf PANOS_VSYS
|
||||
fi
|
||||
|
||||
# PANOS_CERTNAME
|
||||
if [ "$PANOS_CERTNAME" ]; then
|
||||
_debug "Detected ENV variable PANOS_CERTNAME. Saving to file."
|
||||
_savedeployconf PANOS_CERTNAME "$PANOS_CERTNAME" 1
|
||||
else
|
||||
_debug "Attempting to load variable PANOS_CERTNAME from file."
|
||||
_getdeployconf PANOS_CERTNAME
|
||||
fi
|
||||
|
||||
#Store variables
|
||||
_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
|
||||
_panos_certname=$PANOS_CERTNAME
|
||||
|
||||
#Test API Key if found. If the key is invalid, the variable _panos_key will be unset.
|
||||
if [ "$_panos_host" ] && [ "$_panos_key" ]; then
|
||||
|
|
@ -270,22 +207,15 @@ 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
|
||||
_panos_certname="$_cdomain"
|
||||
_savedeployconf PANOS_CERTNAME "$_panos_certname" 1
|
||||
fi
|
||||
|
||||
# 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,34 +226,9 @@ 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
|
||||
# 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
|
||||
while [ "$i" -lt $_COMMIT_WAIT_ITERATIONS ]; do
|
||||
deployer job_status
|
||||
if [ "$_commit_job_status" = "OK" ]; then
|
||||
echo "Commit finished!"
|
||||
break
|
||||
fi
|
||||
sleep $_COMMIT_WAIT_INTERVAL
|
||||
i=$((i + 1))
|
||||
done
|
||||
deployer push
|
||||
fi
|
||||
deployer cert
|
||||
deployer key
|
||||
deployer commit
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Deploy certificates to a proxmox backup server using the API.
|
||||
#
|
||||
# Environment variables that can be set are:
|
||||
# `DEPLOY_PROXMOXBS_SERVER`: The hostname of the proxmox backup server. Defaults to
|
||||
# _cdomain.
|
||||
# `DEPLOY_PROXMOXBS_SERVER_PORT`: The port number the management interface is on.
|
||||
# Defaults to 8007.
|
||||
# `DEPLOY_PROXMOXBS_USER`: The user we'll connect as. Defaults to root.
|
||||
# `DEPLOY_PROXMOXBS_USER_REALM`: The authentication realm the user authenticates
|
||||
# with. Defaults to pam.
|
||||
# `DEPLOY_PROXMOXBS_API_TOKEN_NAME`: The name of the API token created for the
|
||||
# user account. Defaults to acme.
|
||||
# `DEPLOY_PROXMOXBS_API_TOKEN_KEY`: The API token. Required.
|
||||
|
||||
proxmoxbs_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug2 _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
# "Sane" defaults.
|
||||
_getdeployconf DEPLOY_PROXMOXBS_SERVER
|
||||
if [ -z "$DEPLOY_PROXMOXBS_SERVER" ]; then
|
||||
_target_hostname="$_cdomain"
|
||||
else
|
||||
_target_hostname="$DEPLOY_PROXMOXBS_SERVER"
|
||||
_savedeployconf DEPLOY_PROXMOXBS_SERVER "$DEPLOY_PROXMOXBS_SERVER"
|
||||
fi
|
||||
_debug2 DEPLOY_PROXMOXBS_SERVER "$_target_hostname"
|
||||
|
||||
_getdeployconf DEPLOY_PROXMOXBS_SERVER_PORT
|
||||
if [ -z "$DEPLOY_PROXMOXBS_SERVER_PORT" ]; then
|
||||
_target_port="8007"
|
||||
else
|
||||
_target_port="$DEPLOY_PROXMOXBS_SERVER_PORT"
|
||||
_savedeployconf DEPLOY_PROXMOXBS_SERVER_PORT "$DEPLOY_PROXMOXBS_SERVER_PORT"
|
||||
fi
|
||||
_debug2 DEPLOY_PROXMOXBS_SERVER_PORT "$_target_port"
|
||||
|
||||
# Complete URL.
|
||||
_target_url="https://${_target_hostname}:${_target_port}/api2/json/nodes/localhost/certificates/custom"
|
||||
_debug TARGET_URL "$_target_url"
|
||||
|
||||
# More "sane" defaults.
|
||||
_getdeployconf DEPLOY_PROXMOXBS_USER
|
||||
if [ -z "$DEPLOY_PROXMOXBS_USER" ]; then
|
||||
_proxmoxbs_user="root"
|
||||
else
|
||||
_proxmoxbs_user="$DEPLOY_PROXMOXBS_USER"
|
||||
_savedeployconf DEPLOY_PROXMOXBS_USER "$DEPLOY_PROXMOXBS_USER"
|
||||
fi
|
||||
_debug2 DEPLOY_PROXMOXBS_USER "$_proxmoxbs_user"
|
||||
|
||||
_getdeployconf DEPLOY_PROXMOXBS_USER_REALM
|
||||
if [ -z "$DEPLOY_PROXMOXBS_USER_REALM" ]; then
|
||||
_proxmoxbs_user_realm="pam"
|
||||
else
|
||||
_proxmoxbs_user_realm="$DEPLOY_PROXMOXBS_USER_REALM"
|
||||
_savedeployconf DEPLOY_PROXMOXBS_USER_REALM "$DEPLOY_PROXMOXBS_USER_REALM"
|
||||
fi
|
||||
_debug2 DEPLOY_PROXMOXBS_USER_REALM "$_proxmoxbs_user_realm"
|
||||
|
||||
_getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME
|
||||
if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_NAME" ]; then
|
||||
_proxmoxbs_api_token_name="acme"
|
||||
else
|
||||
_proxmoxbs_api_token_name="$DEPLOY_PROXMOXBS_API_TOKEN_NAME"
|
||||
_savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME "$DEPLOY_PROXMOXBS_API_TOKEN_NAME"
|
||||
fi
|
||||
_debug2 DEPLOY_PROXMOXBS_API_TOKEN_NAME "$_proxmoxbs_api_token_name"
|
||||
|
||||
# This is required.
|
||||
_getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY
|
||||
if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_KEY" ]; then
|
||||
_err "API key not provided."
|
||||
return 1
|
||||
else
|
||||
_proxmoxbs_api_token_key="$DEPLOY_PROXMOXBS_API_TOKEN_KEY"
|
||||
_savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY "$DEPLOY_PROXMOXBS_API_TOKEN_KEY"
|
||||
fi
|
||||
_debug2 DEPLOY_PROXMOXBS_API_TOKEN_KEY "$_proxmoxbs_api_token_key"
|
||||
|
||||
# PBS API Token header value. Used in "Authorization: PBSAPIToken".
|
||||
_proxmoxbs_header_api_token="${_proxmoxbs_user}@${_proxmoxbs_user_realm}!${_proxmoxbs_api_token_name}:${_proxmoxbs_api_token_key}"
|
||||
_debug2 "Auth Header" "$_proxmoxbs_header_api_token"
|
||||
|
||||
# Ugly. I hate putting heredocs inside functions because heredocs don't
|
||||
# account for whitespace correctly but it _does_ work and is several times
|
||||
# cleaner than anything else I had here.
|
||||
#
|
||||
# This dumps the json payload to a variable that should be passable to the
|
||||
# _psot function.
|
||||
_json_payload=$(
|
||||
cat <<HEREDOC
|
||||
{
|
||||
"certificates": "$(tr '\n' ':' <"$_cfullchain" | sed 's/:/\\n/g')",
|
||||
"key": "$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')",
|
||||
"node":"localhost",
|
||||
"restart":true,
|
||||
"force":true
|
||||
}
|
||||
HEREDOC
|
||||
)
|
||||
_debug2 Payload "$_json_payload"
|
||||
|
||||
_info "Push certificates to server"
|
||||
export HTTPS_INSECURE=1
|
||||
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
|
||||
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
|
||||
_retval=$?
|
||||
# The API errors out with a non-2xx HTTP status and an empty body,
|
||||
# so the status line is checked too, not only the response body.
|
||||
_status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||
_debug2 "HTTP status" "$_status_code"
|
||||
response="$(echo "$response" | _json_decode | _normalizeJson)"
|
||||
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
|
||||
case "$_status_code" in
|
||||
2[0-9][0-9])
|
||||
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
|
||||
_debug3 response "$response"
|
||||
_info "Certificate successfully deployed"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
_err "Certificate deployment failed (HTTP status $_status_code). $message"
|
||||
_debug "Response" "$response"
|
||||
return 1
|
||||
|
||||
}
|
||||
|
|
@ -127,25 +127,6 @@ HEREDOC
|
|||
_info "Push certificates to server"
|
||||
export HTTPS_INSECURE=1
|
||||
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
|
||||
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
|
||||
_retval=$?
|
||||
# The API errors out with a non-2xx HTTP status and an empty body,
|
||||
# so the status line is checked too, not only the response body.
|
||||
_status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||
_debug2 "HTTP status" "$_status_code"
|
||||
response="$(echo "$response" | _json_decode | _normalizeJson)"
|
||||
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
|
||||
case "$_status_code" in
|
||||
2[0-9][0-9])
|
||||
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
|
||||
_debug3 response "$response"
|
||||
_info "Certificate successfully deployed"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
_err "Certificate deployment failed (HTTP status $_status_code). $message"
|
||||
_debug "Response" "$response"
|
||||
return 1
|
||||
_post "$_json_payload" "$_target_url" "" POST "application/json"
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
# 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"
|
||||
|
||||
|
|
@ -46,12 +44,6 @@ 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')
|
||||
|
|
@ -77,7 +69,7 @@ qiniu_deploy() {
|
|||
_debug certId "$_certId"
|
||||
|
||||
## update domain ssl config
|
||||
update_body="{\"certid\":$_certId,\"forceHttps\":$QINIU_FORCE_HTTPS}"
|
||||
update_body="{\"certid\":$_certId,\"forceHttps\":false}"
|
||||
for domain in $QINIU_CDN_DOMAIN; do
|
||||
update_path="/domain/$domain/httpsconf"
|
||||
update_access_token="$(_make_access_token "$update_path")"
|
||||
|
|
|
|||
|
|
@ -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" "base64"
|
||||
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES"
|
||||
|
||||
# push key to routeros
|
||||
if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then
|
||||
|
|
@ -143,10 +143,9 @@ 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=\\\"\\\";\
|
||||
\n/certificate import file-name=$_cdomain.cer passphrase=\\\"\\\";\
|
||||
\n/certificate import file-name=$_cdomain.key passphrase=\\\"\\\";\
|
||||
\ndelay 1;\
|
||||
\n:do {/file remove $_cdomain.cer; } on-error={ }\
|
||||
\n:do {/file remove $_cdomain.key; } on-error={ }\
|
||||
|
|
|
|||
200
deploy/ruckus.sh
200
deploy/ruckus.sh
|
|
@ -1,200 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Here is a script to deploy cert to Ruckus ZoneDirector / Unleashed.
|
||||
#
|
||||
# Public domain, 2024, Tony Rielly <https://github.com/ms264556>
|
||||
#
|
||||
# ```sh
|
||||
# acme.sh --deploy -d ruckus.example.com --deploy-hook ruckus
|
||||
# ```
|
||||
#
|
||||
# Then you need to set the environment variables for the
|
||||
# deploy script to work.
|
||||
#
|
||||
# ```sh
|
||||
# export RUCKUS_HOST=myruckus.example.com
|
||||
# export RUCKUS_USER=myruckususername
|
||||
# export RUCKUS_PASS=myruckuspassword
|
||||
#
|
||||
# acme.sh --deploy -d myruckus.example.com --deploy-hook ruckus
|
||||
# ```
|
||||
#
|
||||
# returns 0 means success, otherwise error.
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
ruckus_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
_err_code=0
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
_getdeployconf RUCKUS_HOST
|
||||
_getdeployconf RUCKUS_USER
|
||||
_getdeployconf RUCKUS_PASS
|
||||
|
||||
if [ -z "$RUCKUS_HOST" ]; then
|
||||
_debug "Using _cdomain as RUCKUS_HOST, please set if not correct."
|
||||
RUCKUS_HOST="$_cdomain"
|
||||
fi
|
||||
|
||||
if [ -z "$RUCKUS_USER" ]; then
|
||||
_err "Need to set the env variable RUCKUS_USER"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$RUCKUS_PASS" ]; then
|
||||
_err "Need to set the env variable RUCKUS_PASS"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_savedeployconf RUCKUS_HOST "$RUCKUS_HOST"
|
||||
_savedeployconf RUCKUS_USER "$RUCKUS_USER"
|
||||
_savedeployconf RUCKUS_PASS "$RUCKUS_PASS"
|
||||
|
||||
_debug RUCKUS_HOST "$RUCKUS_HOST"
|
||||
_debug RUCKUS_USER "$RUCKUS_USER"
|
||||
_secure_debug RUCKUS_PASS "$RUCKUS_PASS"
|
||||
|
||||
export ACME_HTTP_NO_REDIRECTS=1
|
||||
|
||||
_info "Discovering the login URL"
|
||||
_get "https://$RUCKUS_HOST" >/dev/null
|
||||
_login_url="$(_response_header 'Location')"
|
||||
if [ -n "$_login_url" ]; then
|
||||
_login_path=$(echo "$_login_url" | sed 's|https\?://[^/]\+||')
|
||||
if [ -z "$_login_path" ]; then
|
||||
# redirect was to a different host
|
||||
_err "Connection failed: redirected to a different host. Configure Unleashed with a Preferred Master or Management Interface."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "${_login_url}" ]; then
|
||||
_err "Connection failed: couldn't find login page."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_base_url=$(dirname "$_login_url")
|
||||
_login_page=$(basename "$_login_url")
|
||||
|
||||
if [ "$_login_page" = "index.html" ]; then
|
||||
_err "Connection temporarily unavailable: Unleashed Rebuilding."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_login_page" = "wizard.jsp" ]; then
|
||||
_err "Connection failed: Setup Wizard not complete."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Login"
|
||||
_username_encoded="$(printf "%s" "$RUCKUS_USER" | _url_encode)"
|
||||
_password_encoded="$(printf "%s" "$RUCKUS_PASS" | _url_encode)"
|
||||
_login_query="$(printf "%s" "username=${_username_encoded}&password=${_password_encoded}&ok=Log+In")"
|
||||
_post "$_login_query" "$_login_url" >/dev/null
|
||||
|
||||
_login_code="$(_response_code)"
|
||||
if [ "$_login_code" = "200" ]; then
|
||||
_err "Login failed: incorrect credentials."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Collect Session Cookie"
|
||||
_H1="Cookie: $(_response_cookie)"
|
||||
export _H1
|
||||
_info "Collect CSRF Token"
|
||||
_H2="X-CSRF-Token: $(_response_header 'HTTP_X_CSRF_TOKEN')"
|
||||
export _H2
|
||||
|
||||
if _isRSA "$_ckey" >/dev/null 2>&1; then
|
||||
_debug "Using RSA certificate."
|
||||
else
|
||||
_info "Verifying ECC certificate support."
|
||||
|
||||
_ul_version="$(_get_unleashed_version)"
|
||||
if [ -z "$_ul_version" ]; then
|
||||
_err "Your controller doesn't support ECC certificates. Please deploy an RSA certificate."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_ul_version_major="$(echo "$_ul_version" | cut -d . -f 1)"
|
||||
_ul_version_minor="$(echo "$_ul_version" | cut -d . -f 2)"
|
||||
if [ "$_ul_version_major" -lt "200" ]; then
|
||||
_err "ZoneDirector doesn't support ECC certificates. Please deploy an RSA certificate."
|
||||
return 1
|
||||
elif [ "$_ul_version_minor" -lt "13" ]; then
|
||||
_err "Unleashed $_ul_version_major.$_ul_version_minor doesn't support ECC certificates. Please deploy an RSA certificate or upgrade to Unleashed 200.13+."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "ECC certificates OK for Unleashed $_ul_version_major.$_ul_version_minor."
|
||||
fi
|
||||
|
||||
_info "Uploading certificate"
|
||||
_post_upload "uploadcert" "$_cfullchain"
|
||||
|
||||
_info "Uploading private key"
|
||||
_post_upload "uploadprivatekey" "$_ckey"
|
||||
|
||||
_info "Replacing certificate"
|
||||
_replace_cert_ajax='<ajax-request action="docmd" comp="system" updater="rid.0.5" xcmd="replace-cert" checkAbility="6" timeout="-1"><xcmd cmd="replace-cert" cn="'$RUCKUS_HOST'"/></ajax-request>'
|
||||
_post "$_replace_cert_ajax" "$_base_url/_cmdstat.jsp" >/dev/null
|
||||
|
||||
_info "Rebooting"
|
||||
_cert_reboot_ajax='<ajax-request action="docmd" comp="worker" updater="rid.0.5" xcmd="cert-reboot" checkAbility="6"><xcmd cmd="cert-reboot" action="undefined"/></ajax-request>'
|
||||
_post "$_cert_reboot_ajax" "$_base_url/_cmdstat.jsp" >/dev/null
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_response_code() {
|
||||
_egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n" | _egrep_o "^[0-9]*"
|
||||
}
|
||||
|
||||
_response_header() {
|
||||
grep <"$HTTP_HEADER" -i "^$1:" | cut -d ':' -f 2- | tr -d "\r\n\t "
|
||||
}
|
||||
|
||||
_response_cookie() {
|
||||
_response_header 'Set-Cookie' | sed 's/;.*//'
|
||||
}
|
||||
|
||||
_get_unleashed_version() {
|
||||
_post '<ajax-request action="getstat" comp="system"><sysinfo/></ajax-request>' "$_base_url/_cmdstat.jsp" | _egrep_o "version-num=\"[^\"]*\"" | cut -d '"' -f 2
|
||||
}
|
||||
|
||||
_post_upload() {
|
||||
_post_action="$1"
|
||||
_post_file="$2"
|
||||
|
||||
_post_boundary="----FormBoundary$(date "+%s%N")"
|
||||
|
||||
_post_data="$({
|
||||
printf -- "--%s\r\n" "$_post_boundary"
|
||||
printf -- "Content-Disposition: form-data; name=\"u\"; filename=\"%s\"\r\n" "$_post_action"
|
||||
printf -- "Content-Type: application/octet-stream\r\n\r\n"
|
||||
printf -- "%s\r\n" "$(cat "$_post_file")"
|
||||
|
||||
printf -- "--%s\r\n" "$_post_boundary"
|
||||
printf -- "Content-Disposition: form-data; name=\"action\"\r\n\r\n"
|
||||
printf -- "%s\r\n" "$_post_action"
|
||||
|
||||
printf -- "--%s\r\n" "$_post_boundary"
|
||||
printf -- "Content-Disposition: form-data; name=\"callback\"\r\n\r\n"
|
||||
printf -- "%s\r\n" "uploader_$_post_action"
|
||||
|
||||
printf -- "--%s--\r\n\r\n" "$_post_boundary"
|
||||
})"
|
||||
|
||||
_post "$_post_data" "$_base_url/_upload.jsp?request_type=xhr" "" "" "multipart/form-data; boundary=$_post_boundary" >/dev/null
|
||||
}
|
||||
280
deploy/shelly.sh
280
deploy/shelly.sh
|
|
@ -1,280 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Here is a script to deploy cert to a Shelly Gen3+ device.
|
||||
# Deploy the HTTPS server certificate to a Shelly device on the local network.
|
||||
#
|
||||
# ```sh
|
||||
# export SHELLY_HOST=192.168.1.100
|
||||
# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
|
||||
# acme.sh --deploy -d shelly.example.com --deploy-hook shelly
|
||||
# ```
|
||||
#
|
||||
# Environment variables:
|
||||
# SHELLY_HOST (required) IP or hostname of the Shelly device
|
||||
# SHELLY_PASSWORD (optional) Admin password for digest authentication.
|
||||
# Omit if auth is disabled on the device.
|
||||
# SHELLY_USER (optional) Username for auth. Default: admin
|
||||
# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot.
|
||||
# Default: 1 (reboot after upload)
|
||||
#
|
||||
# Requirements:
|
||||
# - Shelly Gen3+ device (Gen4 recommended)
|
||||
# - Firmware 2.0.0+ for HTTPS server certificate support
|
||||
# - curl or wget
|
||||
# - openssl (for SHA-256 digest and random cnonce)
|
||||
#
|
||||
# The device must be reachable via HTTP on the local network.
|
||||
# The hook uploads the fullchain.pem and private key,
|
||||
# then reboots the device to apply the new certificate.
|
||||
#
|
||||
# Authentication uses standard RFC 7616 HTTP Digest (SHA-256) since
|
||||
# firmware 2.0.0. The JSON-RPC auth object is not used for HTTP transport.
|
||||
#
|
||||
# returns 0 means success, otherwise error.
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
shelly_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
_getdeployconf SHELLY_HOST
|
||||
_getdeployconf SHELLY_PASSWORD
|
||||
_getdeployconf SHELLY_USER
|
||||
_getdeployconf SHELLY_REBOOT
|
||||
|
||||
_debug SHELLY_HOST "$SHELLY_HOST"
|
||||
_debug SHELLY_USER "$SHELLY_USER"
|
||||
_secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD"
|
||||
_debug SHELLY_REBOOT "$SHELLY_REBOOT"
|
||||
|
||||
if [ -z "$SHELLY_HOST" ]; then
|
||||
_err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device."
|
||||
return 1
|
||||
fi
|
||||
|
||||
SHELLY_USER="${SHELLY_USER:-admin}"
|
||||
SHELLY_REBOOT="${SHELLY_REBOOT:-1}"
|
||||
|
||||
_savedeployconf SHELLY_HOST "$SHELLY_HOST"
|
||||
_savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD"
|
||||
_savedeployconf SHELLY_USER "$SHELLY_USER"
|
||||
_savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT"
|
||||
|
||||
# --- Auth handshake (only if password is set) ---
|
||||
_shelly_auth_header=""
|
||||
if [ -n "$SHELLY_PASSWORD" ]; then
|
||||
_info "Authenticating to Shelly device at $SHELLY_HOST"
|
||||
if ! _shelly_handshake; then
|
||||
_err "Authentication handshake failed. Check SHELLY_PASSWORD and device accessibility."
|
||||
return 1
|
||||
fi
|
||||
_info "Authentication successful"
|
||||
fi
|
||||
|
||||
# --- Upload certificate ---
|
||||
_info "Uploading certificate to Shelly device at $SHELLY_HOST"
|
||||
if ! _shelly_upload_cert; then
|
||||
_err "Certificate upload failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# --- Upload key ---
|
||||
_info "Uploading private key to Shelly device"
|
||||
if ! _shelly_upload_key; then
|
||||
_err "Private key upload failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Certificate and key uploaded successfully"
|
||||
|
||||
# --- Reboot ---
|
||||
if [ "$SHELLY_REBOOT" != "0" ]; then
|
||||
_info "Rebooting Shelly device to apply certificate"
|
||||
# Reboot may close the connection before sending a response
|
||||
_shelly_rpc "Shelly.Reboot" '{}' || _debug "Reboot may have closed connection (expected)"
|
||||
_info "Reboot command sent. Device will restart shortly."
|
||||
else
|
||||
_info "Skipping reboot (SHELLY_REBOOT=0). Certificate will apply on next restart."
|
||||
fi
|
||||
|
||||
# Clear auth header so it does not leak to other hooks
|
||||
export _H1=""
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Helper functions ---
|
||||
|
||||
# Perform RFC 7616 HTTP Digest auth handshake.
|
||||
# Sets _shelly_auth_header on success (the Authorization header value).
|
||||
_shelly_handshake() {
|
||||
_inithttp
|
||||
|
||||
_debug "Probing device for auth challenge"
|
||||
|
||||
# Use a protected method (Shelly.GetStatus) to trigger 401.
|
||||
# Shelly.GetDeviceInfo is excluded from auth and would miss the challenge.
|
||||
_post '{"id":1,"method":"Shelly.GetStatus"}' \
|
||||
"http://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||
|
||||
# Detect auth from HTTP status line rather than response body
|
||||
if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then
|
||||
# No auth challenge — device accepted the request without credentials
|
||||
_debug "Device responded without auth challenge. Proceeding without auth."
|
||||
return 0
|
||||
fi
|
||||
|
||||
_shelly_realm="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*realm="//;s/".*//')"
|
||||
_shelly_nonce="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*nonce="//;s/".*//')"
|
||||
_shelly_qop="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*qop="//;s/".*//')"
|
||||
|
||||
if [ -z "$_shelly_nonce" ]; then
|
||||
_err "Failed to extract nonce from WWW-Authenticate header. Is SHELLY_PASSWORD correct?"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_shelly_qop="${_shelly_qop:-auth}"
|
||||
|
||||
_debug "Shelly realm: $_shelly_realm"
|
||||
_debug "Shelly qop: $_shelly_qop"
|
||||
_secure_debug "Shelly nonce" "$_shelly_nonce"
|
||||
|
||||
# ha1 = SHA256(username:realm:password)
|
||||
_shelly_ha1="$(printf '%s' "${SHELLY_USER}:${_shelly_realm}:${SHELLY_PASSWORD}" | _digest sha256 hex)"
|
||||
_secure_debug "Shelly ha1" "$_shelly_ha1"
|
||||
|
||||
# Generate client nonce (openssl is required for _digest, so always available)
|
||||
_shelly_cnonce="$(${ACME_OPENSSL_BIN:-openssl} rand -hex 8 2>/dev/null)"
|
||||
_debug "Shelly cnonce: $_shelly_cnonce"
|
||||
|
||||
# Build the digest Authorization header value (stored for reuse)
|
||||
_shelly_nc=1
|
||||
_shelly_build_auth_header
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check whether the HTTP response headers contain a digest auth challenge.
|
||||
# Returns 0 (true) if a 401 with WWW-Authenticate is present.
|
||||
_shelly_has_auth_challenge() {
|
||||
_shelly_headers_file="$1"
|
||||
_shelly_status="$(grep -i '^HTTP/' "$_shelly_headers_file" | _tail_n 1 | awk '{print $2}')"
|
||||
[ "$_shelly_status" = "401" ] && grep -qi '^WWW-Authenticate:' "$_shelly_headers_file"
|
||||
}
|
||||
|
||||
# Build or rebuild the RFC 7616 Authorization header.
|
||||
# Uses: _shelly_ha1, _shelly_nonce, _shelly_cnonce, _shelly_qop, _shelly_realm, _shelly_nc
|
||||
# Sets: _shelly_auth_header
|
||||
_shelly_build_auth_header() {
|
||||
_shelly_nc_hex="$(printf '%08x' "$_shelly_nc")"
|
||||
|
||||
# ha2 = SHA256(POST:/rpc)
|
||||
_shelly_ha2="$(printf '%s' "POST:/rpc" | _digest sha256 hex)"
|
||||
|
||||
# response = SHA256(ha1:nonce:nc:cnonce:qop:ha2)
|
||||
_shelly_digest_response="$(printf '%s' "${_shelly_ha1}:${_shelly_nonce}:${_shelly_nc_hex}:${_shelly_cnonce}:${_shelly_qop}:${_shelly_ha2}" | _digest sha256 hex)"
|
||||
|
||||
# Build the Authorization header value (without the "Authorization: " prefix)
|
||||
_shelly_auth_header="Digest username=\"${SHELLY_USER}\", realm=\"${_shelly_realm}\", nonce=\"${_shelly_nonce}\", uri=\"/rpc\", qop=${_shelly_qop}, nc=${_shelly_nc_hex}, cnonce=\"${_shelly_cnonce}\", response=\"${_shelly_digest_response}\", algorithm=SHA-256"
|
||||
|
||||
_secure_debug "Authorization header" "$_shelly_auth_header"
|
||||
}
|
||||
|
||||
# Make a Shelly JSON-RPC call.
|
||||
# Usage: _shelly_rpc <method> <params_json>
|
||||
# Returns 0 on success, 1 on error.
|
||||
_shelly_rpc() {
|
||||
_shelly_method="$1"
|
||||
_shelly_params="$2"
|
||||
|
||||
_shelly_body='{"id":1,"method":"'"$_shelly_method"'","params":'"$_shelly_params"'}'
|
||||
|
||||
_debug "RPC method: $_shelly_method"
|
||||
_debug2 "RPC body: $_shelly_body"
|
||||
|
||||
# shellcheck disable=SC2090
|
||||
if [ -n "$_shelly_auth_header" ]; then
|
||||
export _H1="Authorization: $_shelly_auth_header"
|
||||
else
|
||||
export _H1=""
|
||||
fi
|
||||
|
||||
_post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||
_shelly_ret=$?
|
||||
|
||||
if [ "$_shelly_ret" != "0" ]; then
|
||||
_err "HTTP request failed for $_shelly_method (curl/wget error $_shelly_ret)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Empty response means something went wrong (auth required but not provided, etc.)
|
||||
if [ -z "$response" ]; then
|
||||
_err "Empty response from Shelly device. If authentication is enabled on the device, set SHELLY_PASSWORD."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Validate response looks like a Shelly JSON-RPC response.
|
||||
# Catches non-JSON responses such as HTTP 429 "Too Many Requests" which
|
||||
# would otherwise pass the empty and "error" checks below.
|
||||
if ! _startswith "$response" '{' || ! _contains "$response" '"id"'; then
|
||||
_err "Invalid response from Shelly device: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for JSON-RPC error in response
|
||||
if _contains "$response" '"error"'; then
|
||||
_err "RPC error from Shelly: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "RPC response: $response"
|
||||
|
||||
# Increment nonce counter and rebuild auth header for next request
|
||||
if [ -n "$_shelly_auth_header" ]; then
|
||||
_shelly_nc=$((_shelly_nc + 1))
|
||||
_shelly_build_auth_header
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Upload the certificate to the device.
|
||||
# Note: We do NOT clear the existing certificate first, because the Shelly
|
||||
# auto-removes all three files (cert, key, CA) when any one is cleared.
|
||||
# Uploading overwrites in place — no clearing needed.
|
||||
_shelly_upload_cert() {
|
||||
_shelly_cert_data="$(_json_encode <"$_cfullchain")"
|
||||
|
||||
_debug "Uploading certificate"
|
||||
if ! _shelly_rpc "Shelly.PutHTTPServerCert" '{"data":"'"$_shelly_cert_data"'"}'; then
|
||||
_err "Failed to upload certificate to device"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Upload the private key to the device.
|
||||
# Note: Do not clear first — see _shelly_upload_cert for rationale.
|
||||
_shelly_upload_key() {
|
||||
_shelly_key_data="$(_json_encode <"$_ckey")"
|
||||
|
||||
_debug "Uploading key"
|
||||
if ! _shelly_rpc "Shelly.PutHTTPServerKey" '{"data":"'"$_shelly_key_data"'"}'; then
|
||||
_err "Failed to upload key to device"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
@ -25,8 +25,7 @@
|
|||
# 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
|
||||
|
|
@ -72,24 +71,6 @@ 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
|
||||
|
|
@ -189,16 +170,10 @@ 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
|
||||
if ! _ssh_deploy; then
|
||||
# in case of an error, remember it, but keep going for the remaining servers
|
||||
_returnCode=1
|
||||
fi
|
||||
_ssh_deploy
|
||||
done
|
||||
|
||||
return $_returnCode
|
||||
}
|
||||
|
||||
_ssh_deploy() {
|
||||
|
|
@ -263,8 +238,6 @@ 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"
|
||||
|
|
@ -453,13 +426,9 @@ _ssh_remote_cmd() {
|
|||
_secure_debug "Remote commands to execute: $_cmd"
|
||||
_info "Submitting sequence of commands to remote server by $_ssh_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
|
||||
# quotations in bash cmd below intended. Squash travis spellcheck error
|
||||
# shellcheck disable=SC2029
|
||||
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" sh -c "'$_cmd'"
|
||||
_err_code="$?"
|
||||
|
||||
if [ "$_err_code" != "0" ]; then
|
||||
|
|
|
|||
|
|
@ -10,89 +10,46 @@
|
|||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
strongswan_deploy() {
|
||||
_cdomain="${1}"
|
||||
_ckey="${2}"
|
||||
_ccert="${3}"
|
||||
_cca="${4}"
|
||||
_cfullchain="${5}"
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_info "Using strongswan"
|
||||
if _exists ipsec; then
|
||||
_ipsec=ipsec
|
||||
elif _exists strongswan; then
|
||||
_ipsec=strongswan
|
||||
fi
|
||||
if _exists swanctl; then
|
||||
_swanctl=swanctl
|
||||
fi
|
||||
# For legacy stroke mode
|
||||
if [ -n "${_ipsec}" ]; then
|
||||
_info "${_ipsec} command detected"
|
||||
_confdir=$(${_ipsec} --confdir)
|
||||
if [ -z "${_confdir}" ]; then
|
||||
_err "no strongswan --confdir is detected"
|
||||
return 1
|
||||
fi
|
||||
_info _confdir "${_confdir}"
|
||||
__deploy_cert "stroke" "${_confdir}" "$@"
|
||||
${_ipsec} reload
|
||||
fi
|
||||
# For modern vici mode
|
||||
if [ -n "${_swanctl}" ]; then
|
||||
_info "${_swanctl} command detected"
|
||||
for _dir in /usr/local/etc/swanctl /etc/swanctl /etc/strongswan/swanctl; do
|
||||
if [ -d ${_dir} ]; then
|
||||
_confdir=${_dir}
|
||||
_info _confdir "${_confdir}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "${_confdir}" ]; then
|
||||
_err "no swanctl config dir is found"
|
||||
return 1
|
||||
fi
|
||||
__deploy_cert "vici" "${_confdir}" "$@"
|
||||
${_swanctl} --load-creds
|
||||
fi
|
||||
if [ -z "${_swanctl}" ] && [ -z "${_ipsec}" ]; then
|
||||
_err "no strongswan or ipsec command is detected"
|
||||
_err "no swanctl is detected"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
__deploy_cert() {
|
||||
_swan_mode="${1}"
|
||||
_confdir="${2}"
|
||||
_cdomain="${3}"
|
||||
_ckey="${4}"
|
||||
_ccert="${5}"
|
||||
_cca="${6}"
|
||||
_cfullchain="${7}"
|
||||
_debug _cdomain "${_cdomain}"
|
||||
_debug _ckey "${_ckey}"
|
||||
_debug _ccert "${_ccert}"
|
||||
_debug _cca "${_cca}"
|
||||
_debug _cfullchain "${_cfullchain}"
|
||||
_debug _swan_mode "${_swan_mode}"
|
||||
_debug _confdir "${_confdir}"
|
||||
if [ "${_swan_mode}" = "vici" ]; then
|
||||
_dir_private="private"
|
||||
_dir_cert="x509"
|
||||
_dir_ca="x509ca"
|
||||
elif [ "${_swan_mode}" = "stroke" ]; then
|
||||
_dir_private="ipsec.d/private"
|
||||
_dir_cert="ipsec.d/certs"
|
||||
_dir_ca="ipsec.d/cacerts"
|
||||
if [ -x /usr/sbin/ipsec ]; then
|
||||
_ipsec=/usr/sbin/ipsec
|
||||
elif [ -x /usr/sbin/strongswan ]; then
|
||||
_ipsec=/usr/sbin/strongswan
|
||||
elif [ -x /usr/local/sbin/ipsec ]; then
|
||||
_ipsec=/usr/local/sbin/ipsec
|
||||
else
|
||||
_err "unknown StrongSwan mode ${_swan_mode}"
|
||||
_err "no strongswan or ipsec command is detected"
|
||||
return 1
|
||||
fi
|
||||
cat "${_ckey}" >"${_confdir}/${_dir_private}/$(basename "${_ckey}")"
|
||||
cat "${_ccert}" >"${_confdir}/${_dir_cert}/$(basename "${_ccert}")"
|
||||
cat "${_cca}" >"${_confdir}/${_dir_ca}/$(basename "${_cca}")"
|
||||
if [ "${_swan_mode}" = "stroke" ]; then
|
||||
cat "${_cfullchain}" >"${_confdir}/${_dir_ca}/$(basename "${_cfullchain}")"
|
||||
|
||||
_info _ipsec "$_ipsec"
|
||||
|
||||
_confdir=$($_ipsec --confdir)
|
||||
if [ $? -ne 0 ] || [ -z "$_confdir" ]; then
|
||||
_err "no strongswan --confdir is detected"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info _confdir "$_confdir"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
cat "$_ckey" >"${_confdir}/ipsec.d/private/$(basename "$_ckey")"
|
||||
cat "$_ccert" >"${_confdir}/ipsec.d/certs/$(basename "$_ccert")"
|
||||
cat "$_cca" >"${_confdir}/ipsec.d/cacerts/$(basename "$_cca")"
|
||||
cat "$_cfullchain" >"${_confdir}/ipsec.d/cacerts/$(basename "$_cfullchain")"
|
||||
|
||||
$_ipsec reload
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
#!/bin/bash
|
||||
|
||||
################################################################################
|
||||
# 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 create temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
|
||||
_err "Missing required tools to creat 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
|
||||
|
|
@ -113,9 +113,9 @@ synology_dsm_deploy() {
|
|||
|
||||
# Default values for scheme, hostname and port
|
||||
# Defaulting to localhost and http, because it's localhost…
|
||||
[ -n "$SYNO_SCHEME" ] || SYNO_SCHEME=http
|
||||
[ -n "$SYNO_HOSTNAME" ] || SYNO_HOSTNAME=localhost
|
||||
[ -n "$SYNO_PORT" ] || SYNO_PORT=5000
|
||||
[ -n "$SYNO_SCHEME" ] || SYNO_SCHEME="http"
|
||||
[ -n "$SYNO_HOSTNAME" ] || SYNO_HOSTNAME="localhost"
|
||||
[ -n "$SYNO_PORT" ] || SYNO_PORT="5000"
|
||||
_savedeployconf SYNO_SCHEME "$SYNO_SCHEME"
|
||||
_savedeployconf SYNO_HOSTNAME "$SYNO_HOSTNAME"
|
||||
_savedeployconf SYNO_PORT "$SYNO_PORT"
|
||||
|
|
@ -186,8 +186,8 @@ synology_dsm_deploy() {
|
|||
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
|
||||
_getdeployconf SYNO_LOCAL_HOSTNAME
|
||||
_debug SYNO_LOCAL_HOSTNAME "${SYNO_LOCAL_HOSTNAME:-}"
|
||||
if [ "$SYNO_HOSTNAME" != "localhost" ] && [ "$SYNO_HOSTNAME" != "127.0.0.1" ]; then
|
||||
if [ "$SYNO_LOCAL_HOSTNAME" != "1" ]; then
|
||||
if [ "$SYNO_LOCAL_HOSTNAME" != "1" ] && [ "$SYNO_LOCAL_HOSTNAME" == "$SYNO_HOSTNAME" ]; then
|
||||
if [ "$SYNO_HOSTNAME" != "localhost" ] && [ "$SYNO_HOSTNAME" != "127.0.0.1" ]; then
|
||||
_err "SYNO_USE_TEMP_ADMIN=1 only support local deployment, though if you are sure that the hostname $SYNO_HOSTNAME is targeting to your **current local machine**, execute 'export SYNO_LOCAL_HOSTNAME=1' then rerun."
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -234,11 +234,11 @@ synology_dsm_deploy() {
|
|||
fi
|
||||
fi
|
||||
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[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 -Eo '[0-9]+')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[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 be 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 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."
|
||||
|
|
@ -320,10 +320,10 @@ synology_dsm_deploy() {
|
|||
_cleardeployconf SYNO_DEVICE_ID
|
||||
_cleardeployconf SYNO_DEVICE_NAME
|
||||
_savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
|
||||
_savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME"
|
||||
_savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_HOSTNAME"
|
||||
else
|
||||
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME" "base64"
|
||||
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" "base64"
|
||||
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME"
|
||||
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD"
|
||||
_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 -Eo '[0-9]+')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
|
||||
_debug2 error_code "$error_code"
|
||||
if [ -n "$error_code" ]; then
|
||||
if [ "$error_code" -eq 105 ]; then
|
||||
|
|
@ -344,7 +344,6 @@ synology_dsm_deploy() {
|
|||
else
|
||||
_err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more."
|
||||
fi
|
||||
_logout
|
||||
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -354,8 +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."
|
||||
_logout
|
||||
_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
|
||||
|
|
@ -389,15 +387,15 @@ synology_dsm_deploy() {
|
|||
if echo "$response" | grep '"restart_httpd":true' >/dev/null; then
|
||||
_info "Restart HTTP services succeeded."
|
||||
else
|
||||
_info "Restart HTTP services not necessary."
|
||||
_info "Restart HTTP services failed."
|
||||
fi
|
||||
_logout
|
||||
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
||||
_logout
|
||||
return 0
|
||||
else
|
||||
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
||||
_err "Unable to update certificate, got error response: $response."
|
||||
_logout
|
||||
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
|
@ -405,8 +403,6 @@ synology_dsm_deploy() {
|
|||
#################### Private functions below ##################################
|
||||
_logout() {
|
||||
# Logout CERT user only to not occupy a permanent session, e.g. in DSM's "Connected Users" widget (based on previous variables)
|
||||
# Must be called before _temp_admin_cleanup: once the temp admin is deleted, its session can no longer be logged out.
|
||||
# Note: this overwrites $response, so print any error message that needs it before calling.
|
||||
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid")
|
||||
_debug3 response "$response"
|
||||
}
|
||||
|
|
@ -415,7 +411,7 @@ _temp_admin_create() {
|
|||
_username="$1"
|
||||
_password="$2"
|
||||
synouser --del "$_username" >/dev/null 2>/dev/null
|
||||
synouser --add "$_username" "$_password" "" 0 "" 0 >/dev/null
|
||||
synouser --add "$_username" "$_password" "" 0 "scruelt@hotmail.com" 0 >/dev/null
|
||||
}
|
||||
|
||||
_temp_admin_cleanup() {
|
||||
|
|
@ -428,6 +424,11 @@ _temp_admin_cleanup() {
|
|||
fi
|
||||
}
|
||||
|
||||
#_cleardeployconf key
|
||||
_cleardeployconf() {
|
||||
_cleardomainconf "SAVED_$1"
|
||||
}
|
||||
|
||||
# key
|
||||
_check2cleardeployconfexp() {
|
||||
_key="$1"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
#
|
||||
# Following environment variables must be set:
|
||||
#
|
||||
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI>"
|
||||
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
|
||||
#
|
||||
# The following environmental variables may be set if you don't like their
|
||||
# default values:
|
||||
|
|
@ -64,20 +64,6 @@ truenas_deploy() {
|
|||
_response=$(_get "$_api_url/system/state")
|
||||
_info "TrueNAS system state: $_response."
|
||||
|
||||
_info "Getting TrueNAS version"
|
||||
_response=$(_get "$_api_url/system/version")
|
||||
|
||||
if echo "$_response" | grep -q "SCALE"; then
|
||||
_truenas_os=$(echo "$_response" | cut -d '-' -f 2)
|
||||
_truenas_version=$(echo "$_response" | cut -d '-' -f 3 | tr -d '"' | cut -d '.' -f 1,2)
|
||||
else
|
||||
_truenas_os="unknown"
|
||||
_truenas_version="unknown"
|
||||
fi
|
||||
|
||||
_info "Detected TrueNAS system os: $_truenas_os"
|
||||
_info "Detected TrueNAS system version: $_truenas_version"
|
||||
|
||||
if [ -z "$_response" ]; then
|
||||
_err "Unable to authenticate to $_api_url."
|
||||
_err 'Check your connection settings are correct, e.g.'
|
||||
|
|
@ -129,106 +115,27 @@ truenas_deploy() {
|
|||
|
||||
_debug3 _activate_result "$_activate_result"
|
||||
|
||||
_truenas_version_23_10="23.10"
|
||||
_truenas_version_24_10="24.10"
|
||||
_info "Checking if WebDAV certificate is the same as the TrueNAS web UI"
|
||||
_webdav_list=$(_get "$_api_url/webdav")
|
||||
_webdav_cert_id=$(echo "$_webdav_list" | grep '"certssl":' | tr -d -- '"certsl: ,')
|
||||
|
||||
_check_version=$(printf "%s\n%s" "$_truenas_version_23_10" "$_truenas_version" | sort -V | head -n 1)
|
||||
if [ "$_truenas_os" != "SCALE" ] || [ "$_check_version" != "$_truenas_version_23_10" ]; then
|
||||
_info "Checking if WebDAV certificate is the same as the TrueNAS web UI"
|
||||
_webdav_list=$(_get "$_api_url/webdav")
|
||||
_webdav_cert_id=$(echo "$_webdav_list" | grep '"certssl":' | tr -d -- '"certsl: ,')
|
||||
|
||||
if [ "$_webdav_cert_id" = "$_active_cert_id" ]; then
|
||||
_info "Updating the WebDAV certificate"
|
||||
_debug _webdav_cert_id "$_webdav_cert_id"
|
||||
_webdav_data="{\"certssl\": \"${_cert_id}\"}"
|
||||
_activate_webdav_cert="$(_post "$_webdav_data" "$_api_url/webdav" "" "PUT" "application/json")"
|
||||
_webdav_new_cert_id=$(echo "$_activate_webdav_cert" | _json_decode | grep '"certssl":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
|
||||
if [ "$_webdav_new_cert_id" -eq "$_cert_id" ]; then
|
||||
_info "WebDAV certificate updated successfully"
|
||||
else
|
||||
_err "Unable to set WebDAV certificate"
|
||||
_debug3 _activate_webdav_cert "$_activate_webdav_cert"
|
||||
_debug3 _webdav_new_cert_id "$_webdav_new_cert_id"
|
||||
return 1
|
||||
fi
|
||||
if [ "$_webdav_cert_id" = "$_active_cert_id" ]; then
|
||||
_info "Updating the WebDAV certificate"
|
||||
_debug _webdav_cert_id "$_webdav_cert_id"
|
||||
_webdav_data="{\"certssl\": \"${_cert_id}\"}"
|
||||
_activate_webdav_cert="$(_post "$_webdav_data" "$_api_url/webdav" "" "PUT" "application/json")"
|
||||
_webdav_new_cert_id=$(echo "$_activate_webdav_cert" | _json_decode | grep '"certssl":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
|
||||
if [ "$_webdav_new_cert_id" -eq "$_cert_id" ]; then
|
||||
_info "WebDAV certificate updated successfully"
|
||||
else
|
||||
_err "Unable to set WebDAV certificate"
|
||||
_debug3 _activate_webdav_cert "$_activate_webdav_cert"
|
||||
_debug3 _webdav_new_cert_id "$_webdav_new_cert_id"
|
||||
else
|
||||
_info "WebDAV certificate is not configured or is not the same as TrueNAS web UI"
|
||||
fi
|
||||
|
||||
_info "Checking if S3 certificate is the same as the TrueNAS web UI"
|
||||
_s3_list=$(_get "$_api_url/s3")
|
||||
_s3_cert_id=$(echo "$_s3_list" | grep '"certificate":' | tr -d -- '"certifa:_ ,')
|
||||
|
||||
if [ "$_s3_cert_id" = "$_active_cert_id" ]; then
|
||||
_info "Updating the S3 certificate"
|
||||
_debug _s3_cert_id "$_s3_cert_id"
|
||||
_s3_data="{\"certificate\": \"${_cert_id}\"}"
|
||||
_activate_s3_cert="$(_post "$_s3_data" "$_api_url/s3" "" "PUT" "application/json")"
|
||||
_s3_new_cert_id=$(echo "$_activate_s3_cert" | _json_decode | grep '"certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
|
||||
if [ "$_s3_new_cert_id" -eq "$_cert_id" ]; then
|
||||
_info "S3 certificate updated successfully"
|
||||
else
|
||||
_err "Unable to set S3 certificate"
|
||||
_debug3 _activate_s3_cert "$_activate_s3_cert"
|
||||
_debug3 _s3_new_cert_id "$_s3_new_cert_id"
|
||||
return 1
|
||||
fi
|
||||
_debug3 _activate_s3_cert "$_activate_s3_cert"
|
||||
else
|
||||
_info "S3 certificate is not configured or is not the same as TrueNAS web UI"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$_truenas_os" = "SCALE" ]; then
|
||||
_check_version=$(printf "%s\n%s" "$_truenas_version_24_10" "$_truenas_version" | sort -V | head -n 1)
|
||||
if [ "$_check_version" != "$_truenas_version_24_10" ]; then
|
||||
_info "Checking if any chart release Apps is using the same certificate as TrueNAS web UI. Tool 'jq' is required"
|
||||
if _exists jq; then
|
||||
_info "Query all chart release"
|
||||
_release_list=$(_get "$_api_url/chart/release")
|
||||
_related_name_list=$(printf "%s" "$_release_list" | jq -r "[.[] | {name,certId: .config.ingress?.main.tls[]?.scaleCert} | select(.certId==$_active_cert_id) | .name ] | unique")
|
||||
_release_length=$(printf "%s" "$_related_name_list" | jq -r "length")
|
||||
_info "Found $_release_length related chart release in list: $_related_name_list"
|
||||
for i in $(seq 0 $((_release_length - 1))); do
|
||||
_release_name=$(echo "$_related_name_list" | jq -r ".[$i]")
|
||||
_info "Updating certificate from $_active_cert_id to $_cert_id for chart release: $_release_name"
|
||||
#Read the chart release configuration
|
||||
_chart_config=$(printf "%s" "$_release_list" | jq -r ".[] | select(.name==\"$_release_name\")")
|
||||
#Replace the old certificate id with the new one in path .config.ingress.main.tls[].scaleCert. Then update .config.ingress
|
||||
_updated_chart_config=$(printf "%s" "$_chart_config" | jq "(.config.ingress?.main.tls[]? | select(.scaleCert==$_active_cert_id) | .scaleCert ) |= $_cert_id | .config.ingress ")
|
||||
_update_chart_result="$(_post "{\"values\" : { \"ingress\" : $_updated_chart_config } }" "$_api_url/chart/release/id/$_release_name" "" "PUT" "application/json")"
|
||||
_debug3 _update_chart_result "$_update_chart_result"
|
||||
done
|
||||
else
|
||||
_info "Tool 'jq' does not exists, skip chart release checking"
|
||||
fi
|
||||
else
|
||||
_info "Checking if any app is using the same certificate as TrueNAS web UI. Tool 'jq' is required"
|
||||
if _exists jq; then
|
||||
_info "Query all apps"
|
||||
_app_list=$(_get "$_api_url/app")
|
||||
_app_id_list=$(printf "%s" "$_app_list" | jq -r '.[].name')
|
||||
_app_length=$(echo "$_app_id_list" | wc -l)
|
||||
_info "Found $_app_length apps"
|
||||
_info "Checking for each app if an update is needed"
|
||||
for i in $(seq 1 "$_app_length"); do
|
||||
_app_id=$(echo "$_app_id_list" | sed -n "${i}p")
|
||||
_app_config="$(_post "\"$_app_id\"" "$_api_url/app/config" "" "POST" "application/json")"
|
||||
# Check if the app use the same certificate TrueNAS web UI
|
||||
_app_active_cert_config=$(echo "$_app_config" | tr -d '\000-\037' | _json_decode | jq -r ".ix_certificates[\"$_active_cert_id\"]")
|
||||
if [ "$_app_active_cert_config" != "null" ]; then
|
||||
_info "Updating certificate from $_active_cert_id to $_cert_id for app: $_app_id"
|
||||
#Replace the old certificate id with the new one in path
|
||||
_update_app_result="$(_post "{\"values\" : { \"network\": { \"certificate_id\": $_cert_id } } }" "$_api_url/app/id/$_app_id" "" "PUT" "application/json")"
|
||||
_debug3 _update_app_result "$_update_app_result"
|
||||
fi
|
||||
done
|
||||
else
|
||||
_info "Tool 'jq' does not exists, skip app checking"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
_debug3 _webdav_new_cert_id "$_webdav_new_cert_id"
|
||||
else
|
||||
_info "WebDAV certificate is not configured or is not the same as TrueNAS web UI"
|
||||
fi
|
||||
|
||||
_info "Checking if FTP certificate is the same as the TrueNAS web UI"
|
||||
|
|
@ -254,6 +161,50 @@ truenas_deploy() {
|
|||
_info "FTP certificate is not configured or is not the same as TrueNAS web UI"
|
||||
fi
|
||||
|
||||
_info "Checking if S3 certificate is the same as the TrueNAS web UI"
|
||||
_s3_list=$(_get "$_api_url/s3")
|
||||
_s3_cert_id=$(echo "$_s3_list" | grep '"certificate":' | tr -d -- '"certifa:_ ,')
|
||||
|
||||
if [ "$_s3_cert_id" = "$_active_cert_id" ]; then
|
||||
_info "Updating the S3 certificate"
|
||||
_debug _s3_cert_id "$_s3_cert_id"
|
||||
_s3_data="{\"certificate\": \"${_cert_id}\"}"
|
||||
_activate_s3_cert="$(_post "$_s3_data" "$_api_url/s3" "" "PUT" "application/json")"
|
||||
_s3_new_cert_id=$(echo "$_activate_s3_cert" | _json_decode | grep '"certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
|
||||
if [ "$_s3_new_cert_id" -eq "$_cert_id" ]; then
|
||||
_info "S3 certificate updated successfully"
|
||||
else
|
||||
_err "Unable to set S3 certificate"
|
||||
_debug3 _activate_s3_cert "$_activate_s3_cert"
|
||||
_debug3 _s3_new_cert_id "$_s3_new_cert_id"
|
||||
return 1
|
||||
fi
|
||||
_debug3 _activate_s3_cert "$_activate_s3_cert"
|
||||
else
|
||||
_info "S3 certificate is not configured or is not the same as TrueNAS web UI"
|
||||
fi
|
||||
|
||||
_info "Checking if any chart release Apps is using the same certificate as TrueNAS web UI. Tool 'jq' is required"
|
||||
if _exists jq; then
|
||||
_info "Query all chart release"
|
||||
_release_list=$(_get "$_api_url/chart/release")
|
||||
_related_name_list=$(printf "%s" "$_release_list" | jq -r "[.[] | {name,certId: .config.ingress?.main.tls[]?.scaleCert} | select(.certId==$_active_cert_id) | .name ] | unique")
|
||||
_release_length=$(printf "%s" "$_related_name_list" | jq -r "length")
|
||||
_info "Found $_release_length related chart release in list: $_related_name_list"
|
||||
for i in $(seq 0 $((_release_length - 1))); do
|
||||
_release_name=$(echo "$_related_name_list" | jq -r ".[$i]")
|
||||
_info "Updating certificate from $_active_cert_id to $_cert_id for chart release: $_release_name"
|
||||
#Read the chart release configuration
|
||||
_chart_config=$(printf "%s" "$_release_list" | jq -r ".[] | select(.name==\"$_release_name\")")
|
||||
#Replace the old certificate id with the new one in path .config.ingress.main.tls[].scaleCert. Then update .config.ingress
|
||||
_updated_chart_config=$(printf "%s" "$_chart_config" | jq "(.config.ingress?.main.tls[]? | select(.scaleCert==$_active_cert_id) | .scaleCert ) |= $_cert_id | .config.ingress ")
|
||||
_update_chart_result="$(_post "{\"values\" : { \"ingress\" : $_updated_chart_config } }" "$_api_url/chart/release/id/$_release_name" "" "PUT" "application/json")"
|
||||
_debug3 _update_chart_result "$_update_chart_result"
|
||||
done
|
||||
else
|
||||
_info "Tool 'jq' does not exists, skip chart release checking"
|
||||
fi
|
||||
|
||||
_info "Deleting old certificate"
|
||||
_delete_result="$(_post "" "$_api_url/certificate/id/$_active_cert_id" "" "DELETE" "application/json")"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,363 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# TrueNAS deploy script for SCALE/CORE using websocket
|
||||
# It is recommend to use a wildcard certificate
|
||||
#
|
||||
# Websocket Documentation: https://www.truenas.com/docs/api/scale_websocket_api.html
|
||||
#
|
||||
# Tested with TrueNAS Scale - Electric Eel 24.10
|
||||
# Changes certificate in the following services:
|
||||
# - Web UI
|
||||
# - FTP
|
||||
# - iX Apps
|
||||
#
|
||||
# The following environment variables must be set:
|
||||
# ------------------------------------------------
|
||||
#
|
||||
# # 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>"
|
||||
# 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
|
||||
|
||||
# Call websocket method
|
||||
# Usage:
|
||||
# _ws_response=$(_ws_call "math.dummycalc" "'{"x": 4, "y": 5}'")
|
||||
# _info "$_ws_response"
|
||||
#
|
||||
# Output:
|
||||
# {"z": 9}
|
||||
#
|
||||
# Arguments:
|
||||
# $@ - midclt arguments for call
|
||||
#
|
||||
# Returns:
|
||||
# JSON/JOBID
|
||||
_ws_call() {
|
||||
_debug "_ws_call arg1" "$1"
|
||||
_debug "_ws_call arg2" "$2"
|
||||
_debug "_ws_call arg3" "$3"
|
||||
if [ $# -eq 3 ]; then
|
||||
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3")
|
||||
fi
|
||||
if [ $# -eq 2 ]; then
|
||||
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2")
|
||||
fi
|
||||
if [ $# -eq 1 ]; then
|
||||
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1")
|
||||
fi
|
||||
_debug "_ws_response" "$_ws_response"
|
||||
printf "%s" "$_ws_response"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Upload certificate with webclient api
|
||||
_ws_upload_cert() {
|
||||
|
||||
/usr/bin/env python - <<EOF
|
||||
import sys
|
||||
|
||||
from truenas_api_client import Client
|
||||
with Client(uri="$_ws_uri") as c:
|
||||
|
||||
### Login with API key
|
||||
print("I:Trying to upload new certificate...")
|
||||
ret = c.call("auth.login_with_api_key", "${DEPLOY_TRUENAS_APIKEY}")
|
||||
if ret:
|
||||
### upload certificate
|
||||
with open('$1', 'r') as file:
|
||||
fullchain = file.read()
|
||||
with open('$2', 'r') as file:
|
||||
privatekey = file.read()
|
||||
ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey}, job=True)
|
||||
print("R:" + str(ret["id"]))
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("R:0")
|
||||
print("E:_ws_upload_cert error!")
|
||||
sys.exit(7)
|
||||
EOF
|
||||
|
||||
return $?
|
||||
|
||||
}
|
||||
|
||||
# Check argument is a number
|
||||
# Usage:
|
||||
#
|
||||
# Output:
|
||||
# n/a
|
||||
#
|
||||
# Arguments:
|
||||
# $1 - Anything
|
||||
#
|
||||
# Returns:
|
||||
# 0: true
|
||||
# 1: false
|
||||
_ws_check_jobid() {
|
||||
case "$1" in
|
||||
[0-9]*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# Wait for job to finish and return result as JSON
|
||||
# Usage:
|
||||
# _ws_result=$(_ws_get_job_result "$_ws_jobid")
|
||||
# _new_certid=$(printf "%s" "$_ws_result" | jq -r '."id"')
|
||||
#
|
||||
# Output:
|
||||
# JSON result of the job
|
||||
#
|
||||
# Arguments:
|
||||
# $1 - JobID
|
||||
#
|
||||
# Returns:
|
||||
# n/a
|
||||
_ws_get_job_result() {
|
||||
while true; do
|
||||
_sleep 2
|
||||
_ws_response=$(_ws_call "core.get_jobs" "[[\"id\", \"=\", $1]]")
|
||||
if [ "$(printf "%s" "$_ws_response" | jq -r '.[]."state"')" != "RUNNING" ]; then
|
||||
_ws_result="$(printf "%s" "$_ws_response" | jq '.[]."result"')"
|
||||
_debug "_ws_result" "$_ws_result"
|
||||
printf "%s" "$_ws_result"
|
||||
_ws_error="$(printf "%s" "$_ws_response" | jq '.[]."error"')"
|
||||
if [ "$_ws_error" != "null" ]; then
|
||||
_err "Job $1 failed:"
|
||||
_err "$_ws_error"
|
||||
return 7
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
########################
|
||||
### Public functions ###
|
||||
########################
|
||||
|
||||
# truenas_ws_deploy
|
||||
#
|
||||
# Deploy new certificate to TrueNAS services
|
||||
#
|
||||
# Arguments
|
||||
# 1: Domain
|
||||
# 2: Key-File
|
||||
# 3: Certificate-File
|
||||
# 4: CA-File
|
||||
# 5: FullChain-File
|
||||
# Returns:
|
||||
# 0: Success
|
||||
# 1: Missing API Key
|
||||
# 2: TrueNAS not ready
|
||||
# 3: Not a JobID
|
||||
# 4: FTP cert error
|
||||
# 5: WebUI cert error
|
||||
# 6: Job error
|
||||
# 7: WS call error
|
||||
#
|
||||
truenas_ws_deploy() {
|
||||
_domain="$1"
|
||||
_file_key="$2"
|
||||
_file_cert="$3"
|
||||
_file_ca="$4"
|
||||
_file_fullchain="$5"
|
||||
_debug _domain "$_domain"
|
||||
_debug _file_key "$_file_key"
|
||||
_debug _file_cert "$_file_cert"
|
||||
_debug _file_ca "$_file_ca"
|
||||
_debug _file_fullchain "$_file_fullchain"
|
||||
|
||||
########## Environment check
|
||||
|
||||
_info "Checking environment variables..."
|
||||
_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."
|
||||
return 1
|
||||
fi
|
||||
# Check Hostname, default to localhost if not set
|
||||
if [ -z "$DEPLOY_TRUENAS_HOSTNAME" ]; then
|
||||
_info "TrueNAS hostname not set. Using 'localhost'."
|
||||
DEPLOY_TRUENAS_HOSTNAME="localhost"
|
||||
fi
|
||||
# Check protocol, default to ws if not set
|
||||
if [ -z "$DEPLOY_TRUENAS_PROTOCOL" ]; then
|
||||
_info "TrueNAS protocol not set. Using 'ws'."
|
||||
DEPLOY_TRUENAS_PROTOCOL="ws"
|
||||
fi
|
||||
|
||||
# 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"
|
||||
_secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
|
||||
_info "Environment variables: OK"
|
||||
|
||||
########## Health check
|
||||
|
||||
_info "Checking TrueNAS health..."
|
||||
_ws_response=$(_ws_call "system.ready" | tr '[:lower:]' '[:upper:]')
|
||||
_ws_ret=$?
|
||||
if [ $_ws_ret -gt 0 ]; then
|
||||
_err "Error calling system.ready:"
|
||||
_err "$_ws_response"
|
||||
return $_ws_ret
|
||||
fi
|
||||
|
||||
if [ "$_ws_response" != "TRUE" ]; then
|
||||
_err "TrueNAS is not ready."
|
||||
_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
|
||||
|
||||
_info "Gather system info..."
|
||||
_ws_response=$(_ws_call "system.info")
|
||||
_truenas_version=$(printf "%s" "$_ws_response" | jq -r '."version"')
|
||||
_info "TrueNAS version: $_truenas_version"
|
||||
|
||||
########## Gather current certificate
|
||||
|
||||
_info "Gather current WebUI certificate..."
|
||||
_ws_response="$(_ws_call "system.general.config")"
|
||||
_ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
|
||||
_ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."name"')
|
||||
_info "Current WebUI certificate ID: $_ui_certificate_id"
|
||||
_info "Current WebUI certificate name: $_ui_certificate_name"
|
||||
|
||||
########## Upload new certificate
|
||||
|
||||
_info "Upload new certificate..."
|
||||
_certname="acme_$(_utc_date | tr -d '\-\:' | tr ' ' '_')"
|
||||
_info "New WebUI certificate name: $_certname"
|
||||
_debug _certname "$_certname"
|
||||
_ws_out=$(_ws_upload_cert "$_file_fullchain" "$_file_key" "$_certname")
|
||||
|
||||
echo "$_ws_out" | while IFS= read -r LINE; do
|
||||
case "$LINE" in
|
||||
I:*)
|
||||
_info "${LINE#I:}"
|
||||
;;
|
||||
D:*)
|
||||
_debug "${LINE#D:}"
|
||||
;;
|
||||
E*)
|
||||
_err "${LINE#E:}"
|
||||
;;
|
||||
*) ;;
|
||||
|
||||
esac
|
||||
done
|
||||
|
||||
_new_certid=$(echo "$_ws_out" | grep 'R:' | cut -d ':' -f 2)
|
||||
|
||||
_info "New certificate ID: $_new_certid"
|
||||
|
||||
########## FTP
|
||||
|
||||
_info "Replace FTP certificate..."
|
||||
_ws_response=$(_ws_call "ftp.update" "{\"ssltls_certificate\": $_new_certid}")
|
||||
_ftp_certid=$(printf "%s" "$_ws_response" | jq -r '."ssltls_certificate"')
|
||||
if [ "$_ftp_certid" != "$_new_certid" ]; then
|
||||
_err "Cannot set FTP certificate."
|
||||
_debug "_ws_response" "$_ws_response"
|
||||
return 4
|
||||
fi
|
||||
|
||||
########## ix Apps (SCALE only)
|
||||
|
||||
_info "Replace app certificates..."
|
||||
_ws_response=$(_ws_call "app.query")
|
||||
for _app_name in $(printf "%s" "$_ws_response" | jq -r '.[]."name"'); do
|
||||
_info "Checking app $_app_name..."
|
||||
_ws_response=$(_ws_call "app.config" "$_app_name")
|
||||
if [ "$(printf "%s" "$_ws_response" | jq -r '."network" | has("certificate_id")')" = "true" ]; then
|
||||
_info "App has certificate option, setup new certificate..."
|
||||
_info "App will be redeployed after updating the certificate."
|
||||
_ws_jobid=$(_ws_call "app.update" "$_app_name" "{\"values\": {\"network\": {\"certificate_id\": $_new_certid}}}")
|
||||
_debug "_ws_jobid" "$_ws_jobid"
|
||||
if ! _ws_check_jobid "$_ws_jobid"; then
|
||||
_err "No JobID returned from websocket method."
|
||||
return 3
|
||||
fi
|
||||
_ws_result=$(_ws_get_job_result "$_ws_jobid")
|
||||
_ws_ret=$?
|
||||
if [ $_ws_ret -gt 0 ]; then
|
||||
return $_ws_ret
|
||||
fi
|
||||
_debug "_ws_result" "$_ws_result"
|
||||
_info "App certificate replaced."
|
||||
else
|
||||
_info "App has no certificate option, skipping..."
|
||||
fi
|
||||
done
|
||||
|
||||
########## WebUI
|
||||
|
||||
_info "Replace WebUI certificate..."
|
||||
_ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}")
|
||||
_changed_certid=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
|
||||
if [ "$_changed_certid" != "$_new_certid" ]; then
|
||||
_err "WebUI certificate change error.."
|
||||
return 5
|
||||
else
|
||||
_info "WebUI certificate replaced."
|
||||
fi
|
||||
_info "Restarting WebUI..."
|
||||
_ws_response=$(_ws_call "system.general.ui_restart")
|
||||
_info "Waiting for UI restart..."
|
||||
_sleep 15
|
||||
|
||||
########## Certificates
|
||||
|
||||
_info "Deleting old certificate..."
|
||||
_ws_jobid=$(_ws_call "certificate.delete" "$_ui_certificate_id")
|
||||
if ! _ws_check_jobid "$_ws_jobid"; then
|
||||
_err "No JobID returned from websocket method."
|
||||
return 3
|
||||
fi
|
||||
_ws_result=$(_ws_get_job_result "$_ws_jobid")
|
||||
_ws_ret=$?
|
||||
if [ $_ws_ret -gt 0 ]; then
|
||||
return $_ws_ret
|
||||
fi
|
||||
|
||||
_info "Have a nice day...bye!"
|
||||
|
||||
}
|
||||
147
deploy/unifi.sh
147
deploy/unifi.sh
|
|
@ -5,15 +5,6 @@
|
|||
# - self-hosted Unifi Controller
|
||||
# - Unifi Cloud Key (Gen1/2/2+)
|
||||
# - Unifi Cloud Key running UnifiOS (v2.0.0+, Gen2/2+ only)
|
||||
# - Unifi Dream Machine
|
||||
# This has not been tested on other "all-in-one" devices such as
|
||||
# UDM Pro or Unifi Express.
|
||||
#
|
||||
# OS Version v2.0.0+
|
||||
# Network Application version 7.0.0+
|
||||
# OS version ~3.1 removed java and keytool from the UnifiOS.
|
||||
# Using PKCS12 format keystore appears to work fine.
|
||||
#
|
||||
# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/3359
|
||||
|
||||
#returns 0 means success, otherwise error.
|
||||
|
|
@ -30,9 +21,7 @@
|
|||
# Keystore password (built into Unifi Controller, not a user-set password):
|
||||
#DEPLOY_UNIFI_KEYPASS="aircontrolenterprise"
|
||||
# Command to restart Unifi Controller:
|
||||
# DEPLOY_UNIFI_RELOAD="systemctl restart unifi"
|
||||
# System Properties file location for controller
|
||||
#DEPLOY_UNIFI_SYSTEM_PROPERTIES="/usr/lib/unifi/data/system.properties"
|
||||
#DEPLOY_UNIFI_RELOAD="service unifi restart"
|
||||
#
|
||||
# Settings for Unifi Cloud Key Gen1 (nginx admin pages):
|
||||
# Directory where cloudkey.crt and cloudkey.key live:
|
||||
|
|
@ -45,7 +34,7 @@
|
|||
# Directory where unifi-core.crt and unifi-core.key live:
|
||||
#DEPLOY_UNIFI_CORE_CONFIG="/data/unifi-core/config/"
|
||||
# Command to restart unifi-core:
|
||||
# DEPLOY_UNIFI_OS_RELOAD="systemctl restart unifi-core"
|
||||
#DEPLOY_UNIFI_RELOAD="systemctl restart unifi-core"
|
||||
#
|
||||
# At least one of DEPLOY_UNIFI_KEYSTORE, DEPLOY_UNIFI_CLOUDKEY_CERTDIR,
|
||||
# or DEPLOY_UNIFI_CORE_CONFIG must exist to receive the deployed certs.
|
||||
|
|
@ -71,16 +60,12 @@ unifi_deploy() {
|
|||
_getdeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR
|
||||
_getdeployconf DEPLOY_UNIFI_CORE_CONFIG
|
||||
_getdeployconf DEPLOY_UNIFI_RELOAD
|
||||
_getdeployconf DEPLOY_UNIFI_SYSTEM_PROPERTIES
|
||||
_getdeployconf DEPLOY_UNIFI_OS_RELOAD
|
||||
|
||||
_debug2 DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
|
||||
_debug2 DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
|
||||
_debug2 DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
|
||||
_debug2 DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
|
||||
_debug2 DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
|
||||
_debug2 DEPLOY_UNIFI_OS_RELOAD "$DEPLOY_UNIFI_OS_RELOAD"
|
||||
_debug2 DEPLOY_UNIFI_SYSTEM_PROPERTIES "$DEPLOY_UNIFI_SYSTEM_PROPERTIES"
|
||||
|
||||
# Space-separated list of environments detected and installed:
|
||||
_services_updated=""
|
||||
|
|
@ -89,16 +74,14 @@ unifi_deploy() {
|
|||
_reload_cmd=""
|
||||
|
||||
# Unifi Controller environment (self hosted or any Cloud Key) --
|
||||
# auto-detect by file /usr/lib/unifi/data/keystore
|
||||
# auto-detect by file /usr/lib/unifi/data/keystore:
|
||||
_unifi_keystore="${DEPLOY_UNIFI_KEYSTORE:-/usr/lib/unifi/data/keystore}"
|
||||
if [ -f "$_unifi_keystore" ]; then
|
||||
_info "Installing certificate for Unifi Controller (Java keystore)"
|
||||
_debug _unifi_keystore "$_unifi_keystore"
|
||||
if ! _exists keytool; then
|
||||
_do_keytool=0
|
||||
_info "Installing certificate for Unifi Controller (PKCS12 keystore)."
|
||||
else
|
||||
_do_keytool=1
|
||||
_info "Installing certificate for Unifi Controller (Java keystore)"
|
||||
_err "keytool not found"
|
||||
return 1
|
||||
fi
|
||||
if [ ! -w "$_unifi_keystore" ]; then
|
||||
_err "The file $_unifi_keystore is not writable, please change the permission."
|
||||
|
|
@ -109,7 +92,6 @@ unifi_deploy() {
|
|||
|
||||
_debug "Generate import pkcs12"
|
||||
_import_pkcs12="$(_mktemp)"
|
||||
_debug "_toPkcs $_import_pkcs12 $_ckey $_ccert $_cca $_unifi_keypass unifi root"
|
||||
_toPkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca" "$_unifi_keypass" unifi root
|
||||
# shellcheck disable=SC2181
|
||||
if [ "$?" != "0" ]; then
|
||||
|
|
@ -117,79 +99,22 @@ unifi_deploy() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
# Save the existing keystore in case something goes wrong.
|
||||
mv -f "${_unifi_keystore}" "${_unifi_keystore}"_original
|
||||
_info "Previous keystore saved to ${_unifi_keystore}_original."
|
||||
|
||||
if [ "$_do_keytool" -eq 1 ]; then
|
||||
_debug "Import into keystore: $_unifi_keystore"
|
||||
if keytool -importkeystore \
|
||||
-deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \
|
||||
-srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \
|
||||
-alias unifi -noprompt; then
|
||||
_debug "Import keystore success!"
|
||||
else
|
||||
_err "Error importing into Unifi Java keystore."
|
||||
_err "Please re-run with --debug and report a bug."
|
||||
_info "Restoring original keystore."
|
||||
mv -f "${_unifi_keystore}"_original "${_unifi_keystore}"
|
||||
rm "$_import_pkcs12"
|
||||
return 1
|
||||
fi
|
||||
_debug "Import into keystore: $_unifi_keystore"
|
||||
if keytool -importkeystore \
|
||||
-deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \
|
||||
-srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \
|
||||
-alias unifi -noprompt; then
|
||||
_debug "Import keystore success!"
|
||||
rm "$_import_pkcs12"
|
||||
else
|
||||
_debug "Copying new keystore to $_unifi_keystore"
|
||||
cp -f "$_import_pkcs12" "$_unifi_keystore"
|
||||
_err "Error importing into Unifi Java keystore."
|
||||
_err "Please re-run with --debug and report a bug."
|
||||
rm "$_import_pkcs12"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# correct file ownership according to the directory, the keystore is placed in
|
||||
_unifi_keystore_dir=$(dirname "${_unifi_keystore}")
|
||||
# shellcheck disable=SC2012
|
||||
_unifi_keystore_dir_owner=$(ls -ld "${_unifi_keystore_dir}" | awk '{print $3}')
|
||||
# shellcheck disable=SC2012
|
||||
_unifi_keystore_owner=$(ls -l "${_unifi_keystore}" | awk '{print $3}')
|
||||
if ! [ "${_unifi_keystore_owner}" = "${_unifi_keystore_dir_owner}" ]; then
|
||||
_debug "Changing keystore owner to ${_unifi_keystore_dir_owner}"
|
||||
chown "$_unifi_keystore_dir_owner" "${_unifi_keystore}" >/dev/null 2>&1 # fail quietly if we're not running as root
|
||||
fi
|
||||
|
||||
# Update unifi service for certificate cipher compatibility
|
||||
_unifi_system_properties="${DEPLOY_UNIFI_SYSTEM_PROPERTIES:-/usr/lib/unifi/data/system.properties}"
|
||||
if ${ACME_OPENSSL_BIN:-openssl} pkcs12 \
|
||||
-in "$_import_pkcs12" \
|
||||
-password pass:aircontrolenterprise \
|
||||
-nokeys | ${ACME_OPENSSL_BIN:-openssl} x509 -text \
|
||||
-noout | grep -i "signature" | grep -iq ecdsa >/dev/null 2>&1; then
|
||||
if [ -f "$(dirname "${DEPLOY_UNIFI_KEYSTORE}")/system.properties" ]; then
|
||||
_unifi_system_properties="$(dirname "${DEPLOY_UNIFI_KEYSTORE}")/system.properties"
|
||||
else
|
||||
_unifi_system_properties="/usr/lib/unifi/data/system.properties"
|
||||
fi
|
||||
if [ -f "${_unifi_system_properties}" ]; then
|
||||
cp -f "${_unifi_system_properties}" "${_unifi_system_properties}"_original
|
||||
_info "Updating system configuration for cipher compatibility."
|
||||
_info "Saved original system config to ${_unifi_system_properties}_original"
|
||||
sed -i '/unifi\.https\.ciphers/d' "${_unifi_system_properties}"
|
||||
echo "unifi.https.ciphers=ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES128-GCM-SHA256" >>"${_unifi_system_properties}"
|
||||
sed -i '/unifi\.https\.sslEnabledProtocols/d' "${_unifi_system_properties}"
|
||||
echo "unifi.https.sslEnabledProtocols=TLSv1.3,TLSv1.2" >>"${_unifi_system_properties}"
|
||||
_info "System configuration updated."
|
||||
fi
|
||||
fi
|
||||
|
||||
rm "$_import_pkcs12"
|
||||
|
||||
# Restarting unifi-core will bring up unifi, doing it out of order results in
|
||||
# a certificate error, and breaks wifiman.
|
||||
# Restart if we aren't doing Unifi OS (e.g. unifi-core service), otherwise stop for later restart.
|
||||
_unifi_reload="${DEPLOY_UNIFI_RELOAD:-systemctl restart unifi}"
|
||||
if [ ! -f "${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}/unifi-core.key" ]; then
|
||||
_reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_reload"
|
||||
else
|
||||
_info "Stopping Unifi Controller for later restart."
|
||||
_unifi_stop=$(echo "${_unifi_reload}" | sed -e 's/restart/stop/')
|
||||
$_unifi_stop
|
||||
_reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_reload"
|
||||
_info "Unifi Controller stopped."
|
||||
if systemctl -q is-active unifi; then
|
||||
_reload_cmd="${_reload_cmd:+$_reload_cmd && }service unifi restart"
|
||||
fi
|
||||
_services_updated="${_services_updated} unifi"
|
||||
_info "Install Unifi Controller certificate success!"
|
||||
|
|
@ -209,24 +134,13 @@ unifi_deploy() {
|
|||
return 1
|
||||
fi
|
||||
# Cloud Key expects to load the keystore from /etc/ssl/private/unifi.keystore.jks.
|
||||
# It appears that unifi won't start if this is a symlink, so we'll copy it instead.
|
||||
|
||||
# if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then
|
||||
# _err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'"
|
||||
# return 1
|
||||
# fi
|
||||
|
||||
_info "Updating ${_cloudkey_certdir}/unifi.keystore.jks"
|
||||
if [ -e "${_cloudkey_certdir}/unifi.keystore.jks" ]; then
|
||||
if [ -L "${_cloudkey_certdir}/unifi.keystore.jks" ]; then
|
||||
rm -f "${_cloudkey_certdir}/unifi.keystore.jks"
|
||||
else
|
||||
mv "${_cloudkey_certdir}/unifi.keystore.jks" "${_cloudkey_certdir}/unifi.keystore.jks_original"
|
||||
fi
|
||||
# Normally /usr/lib/unifi/data/keystore is a symlink there (so the keystore was
|
||||
# updated above), but if not, we don't know how to handle this installation:
|
||||
if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then
|
||||
_err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cp "${_unifi_keystore}" "${_cloudkey_certdir}/unifi.keystore.jks"
|
||||
|
||||
cat "$_cfullchain" >"${_cloudkey_certdir}/cloudkey.crt"
|
||||
cat "$_ckey" >"${_cloudkey_certdir}/cloudkey.key"
|
||||
(cd "$_cloudkey_certdir" && tar -cf cert.tar cloudkey.crt cloudkey.key unifi.keystore.jks)
|
||||
|
|
@ -251,17 +165,12 @@ unifi_deploy() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
# Save the existing certs in case something goes wrong.
|
||||
cp -f "${_unifi_core_config}"/unifi-core.crt "${_unifi_core_config}"/unifi-core_original.crt
|
||||
cp -f "${_unifi_core_config}"/unifi-core.key "${_unifi_core_config}"/unifi-core_original.key
|
||||
_info "Previous certificate and key saved to ${_unifi_core_config}/unifi-core_original.crt.key."
|
||||
|
||||
cat "$_cfullchain" >"${_unifi_core_config}/unifi-core.crt"
|
||||
cat "$_ckey" >"${_unifi_core_config}/unifi-core.key"
|
||||
|
||||
_unifi_os_reload="${DEPLOY_UNIFI_OS_RELOAD:-systemctl restart unifi-core}"
|
||||
_reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_os_reload"
|
||||
|
||||
if systemctl -q is-active unifi-core; then
|
||||
_reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl restart unifi-core"
|
||||
fi
|
||||
_info "Install UnifiOS certificate success!"
|
||||
_services_updated="${_services_updated} unifi-core"
|
||||
elif [ "$DEPLOY_UNIFI_CORE_CONFIG" ]; then
|
||||
|
|
@ -300,8 +209,6 @@ unifi_deploy() {
|
|||
_savedeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
|
||||
_savedeployconf DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
|
||||
_savedeployconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
|
||||
_savedeployconf DEPLOY_UNIFI_OS_RELOAD "$DEPLOY_UNIFI_OS_RELOAD"
|
||||
_savedeployconf DEPLOY_UNIFI_SYSTEM_PROPERTIES "$DEPLOY_UNIFI_SYSTEM_PROPERTIES"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,307 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
# Deploy hook for UniFi OS Server (self-hosted).
|
||||
#
|
||||
# Supports:
|
||||
# - UniFi OS Server on macOS
|
||||
# - UniFi OS Server on Linux
|
||||
# - UniFi OS Server on Windows should also work (runs under WSL2), but
|
||||
# has not been tested.
|
||||
#
|
||||
# Tested on: Ubuntu 26.04 (remote) and macOS 26.6 (local).
|
||||
#
|
||||
# This is a different product from the Cloud Key / UDM hardware and
|
||||
# self-hosted Unifi Controller covered by the `unifi` deploy hook above
|
||||
# (that hook already covers Cloud Key running UnifiOS v2.0.0+/Gen2/2+) --
|
||||
# this hook targets the separately-installed, self-hosted "UniFi OS Server"
|
||||
# application instead, which stores certificates in its own Postgres
|
||||
# database via a REST API rather than a Java keystore, so the `unifi`
|
||||
# hook's approach does not apply here.
|
||||
#
|
||||
# UniFi OS Server exposes a REST API on its management port (default
|
||||
# 11443) that its own web UI uses for certificate management:
|
||||
# POST /api/auth/login - session login (cookie + JWT)
|
||||
# GET /api/userCertificates - list uploaded certificates
|
||||
# POST /api/userCertificates - upload a new certificate
|
||||
# DELETE /api/userCertificates/{id} - remove a certificate
|
||||
# PUT /api/userCertificates/{id}/status - activate/deactivate a certificate
|
||||
#
|
||||
# This was reverse-engineered from the browser's Network tab while using the
|
||||
# real GUI upload/activate/delete flow -- it is undocumented but is the same
|
||||
# code path the UI uses, so it's far more robust than editing settings.yaml,
|
||||
# http/local-certs.conf, or the underlying Postgres user_certificates table
|
||||
# directly (all of which are also touched by this API, but only as a result
|
||||
# of the app's own internal logic, which handles cert parsing, active-cert
|
||||
# bookkeeping, and nginx config regeneration correctly on its own).
|
||||
#
|
||||
# Auth: POST /api/auth/login returns a `TOKEN` cookie containing a JWT whose
|
||||
# payload has a `csrfToken` claim. That value must be echoed back as the
|
||||
# `x-csrf-token` header on every subsequent state-changing request (a classic
|
||||
# double-submit CSRF pattern). No other cookies were found to be necessary.
|
||||
#
|
||||
# Uses core acme.sh helpers throughout (_post/_get, _json_encode,
|
||||
# _durl_replace_base64, _dbase64, _egrep_o) rather than raw curl -k or
|
||||
# python3, so the wget fallback, --debug tracing, and CA_BUNDLE are all
|
||||
# honored the same as every other hook. The management API's cert is
|
||||
# self-signed (it's a management-only port, not meant for public exposure),
|
||||
# so this hook sets HTTPS_INSECURE=1 itself, scoped to its own subshell (see
|
||||
# acme.sh's per-hook sourcing in _deploy) -- it does not weaken TLS
|
||||
# verification for the rest of the acme.sh run, e.g. the connection to the
|
||||
# ACME CA.
|
||||
#
|
||||
# Design: This hook does not save a certificate ID between renewals. Each
|
||||
# upload gets a name unique to that run: the domain name plus a timestamp.
|
||||
# This name never collides with an entry from a previous deploy. This is
|
||||
# true even if that entry is still active. The hook uploads and activates
|
||||
# the new certificate before it removes any old entries. If a failure
|
||||
# occurs during this process, the server still has a valid, active
|
||||
# certificate. The hook removes old entries only after activation is
|
||||
# complete. It removes only entries whose name starts with the domain name,
|
||||
# because this is the hook's own naming convention. As a result, this step
|
||||
# can only affect entries that this hook created for this domain. It can
|
||||
# never affect a certificate that a user uploaded manually, and it can
|
||||
# never affect a self-signed certificate.
|
||||
#
|
||||
# Settings:
|
||||
# DEPLOY_UNIFIOS_HOST - base URL of the management API
|
||||
# (default: "https://localhost:11443")
|
||||
# DEPLOY_UNIFIOS_USERNAME - UniFi OS Server admin username (required)
|
||||
# DEPLOY_UNIFIOS_PASSWORD - UniFi OS Server admin password (required)
|
||||
#
|
||||
# Example:
|
||||
# export DEPLOY_UNIFIOS_USERNAME="acmeuser"
|
||||
# export DEPLOY_UNIFIOS_PASSWORD="xxxxx"
|
||||
# acme.sh --deploy -d example.com --deploy-hook unifios
|
||||
#
|
||||
# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/7182
|
||||
|
||||
_uos_response_code() {
|
||||
# tr strips the trailing newline along with form feeds; re-terminate
|
||||
# before the second _egrep_o, whose sed fallback (used wherever egrep -o
|
||||
# is unavailable) drops an unterminated final line on some platforms.
|
||||
_uos_code="$(_egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n")"
|
||||
printf '%s\n' "$_uos_code" | _egrep_o "^[0-9][0-9]*"
|
||||
}
|
||||
|
||||
_uos_response_cookie() {
|
||||
# $1 = cookie name
|
||||
grep <"$HTTP_HEADER" -i "^Set-Cookie: *$1=" | _tail_n 1 | _egrep_o "$1=[^;]*" | _head_n 1
|
||||
}
|
||||
|
||||
unifios_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"
|
||||
|
||||
# Scoped to this hook's own subshell -- does not affect the rest of the
|
||||
# acme.sh run (e.g. the connection to the ACME CA).
|
||||
export HTTPS_INSECURE=1
|
||||
|
||||
_getdeployconf DEPLOY_UNIFIOS_HOST
|
||||
DEPLOY_UNIFIOS_HOST="${DEPLOY_UNIFIOS_HOST:-https://localhost:11443}"
|
||||
_savedeployconf DEPLOY_UNIFIOS_HOST "$DEPLOY_UNIFIOS_HOST"
|
||||
_debug DEPLOY_UNIFIOS_HOST "$DEPLOY_UNIFIOS_HOST"
|
||||
|
||||
_getdeployconf DEPLOY_UNIFIOS_USERNAME
|
||||
_getdeployconf DEPLOY_UNIFIOS_PASSWORD
|
||||
|
||||
if [ -z "$DEPLOY_UNIFIOS_USERNAME" ] || [ -z "$DEPLOY_UNIFIOS_PASSWORD" ]; then
|
||||
_err "DEPLOY_UNIFIOS_USERNAME and DEPLOY_UNIFIOS_PASSWORD must be set."
|
||||
return 1
|
||||
fi
|
||||
_debug DEPLOY_UNIFIOS_USERNAME "$DEPLOY_UNIFIOS_USERNAME"
|
||||
_secure_debug DEPLOY_UNIFIOS_PASSWORD "$DEPLOY_UNIFIOS_PASSWORD"
|
||||
|
||||
_info "Logging in to UniFi OS Server API at $DEPLOY_UNIFIOS_HOST..."
|
||||
|
||||
# _json_encode always appends a trailing "\n" escape, even to input with
|
||||
# no trailing newline (it normalizes via `echo`, unconditionally adding
|
||||
# one). That's harmless for the key/cert file content below, which
|
||||
# legitimately ends in a real newline anyway, but wrong for these plain
|
||||
# strings -- strip the spurious escape it leaves behind.
|
||||
_uos_user_json="$(printf '%s' "$DEPLOY_UNIFIOS_USERNAME" | _json_encode)"
|
||||
_uos_user_json="${_uos_user_json%\\n}"
|
||||
_uos_pass_json="$(printf '%s' "$DEPLOY_UNIFIOS_PASSWORD" | _json_encode)"
|
||||
_uos_pass_json="${_uos_pass_json%\\n}"
|
||||
_login_body="{\"username\":\"$_uos_user_json\",\"password\":\"$_uos_pass_json\",\"token\":\"\",\"rememberMe\":false}"
|
||||
|
||||
_login_json="$(_post "$_login_body" "$DEPLOY_UNIFIOS_HOST/api/auth/login" "" "POST" "application/json")"
|
||||
_login_code="$(_uos_response_code)"
|
||||
|
||||
if [ "$_login_code" != "200" ]; then
|
||||
_err "Login failed (HTTP $_login_code)."
|
||||
_err "Response: $_login_json"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Credentials are proven correct now -- save them, rather than only at the
|
||||
# very end, so a later step failing doesn't discard a working login.
|
||||
# base64-encoded: _save_conf wraps values in single quotes with no
|
||||
# escaping, so a literal "'" in the password would otherwise corrupt the
|
||||
# domain conf (see deploy/synology_dsm.sh for the same pattern).
|
||||
_savedeployconf DEPLOY_UNIFIOS_USERNAME "$DEPLOY_UNIFIOS_USERNAME" "base64"
|
||||
_savedeployconf DEPLOY_UNIFIOS_PASSWORD "$DEPLOY_UNIFIOS_PASSWORD" "base64"
|
||||
|
||||
_uos_token="$(_uos_response_cookie TOKEN)"
|
||||
if [ -z "$_uos_token" ]; then
|
||||
_err "Login succeeded but no TOKEN cookie was returned."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_H1="Cookie: $_uos_token"
|
||||
export _H1
|
||||
|
||||
_uos_jwt_payload="$(echo "$_uos_token" | cut -d '=' -f 2- | cut -d '.' -f 2)"
|
||||
_uos_csrf="$(_durl_replace_base64 "$_uos_jwt_payload" | _dbase64 | _egrep_o '"csrfToken":"[^"]*"' | cut -d '"' -f 4)"
|
||||
if [ -z "$_uos_csrf" ]; then
|
||||
_err "Could not extract csrfToken from session token."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_H2="x-csrf-token: $_uos_csrf"
|
||||
export _H2
|
||||
|
||||
_info "Uploading new certificate..."
|
||||
# "name" is a purely cosmetic label -- the server never validates it
|
||||
# against the certificate's actual CN/SAN, and accepts arbitrary text
|
||||
# including spaces (confirmed: a cert for example.com served correctly
|
||||
# after being uploaded under the unrelated name "totally unrelated label").
|
||||
# The only constraint that matters here is uniqueness: the server rejects
|
||||
# a second entry with a name it already has, so a bare domain name would
|
||||
# collide with the previous deploy's entry on every renewal after the
|
||||
# first. A full human-readable timestamp would make that obvious in the
|
||||
# UI, but the certificate list's name column is fixed-width and doesn't
|
||||
# wrap (confirmed against the real UI: a long name overlaps the Expires
|
||||
# column and makes both unreadable), so keep the suffix short instead --
|
||||
# Unix epoch seconds are still unique enough for this purpose.
|
||||
_uos_name="$_cdomain $(_time)"
|
||||
_uos_key_json="$(_json_encode <"$_ckey")"
|
||||
_uos_cert_json="$(_json_encode <"$_cfullchain")"
|
||||
_create_body="{\"name\":\"$_uos_name\",\"key\":\"$_uos_key_json\",\"cert\":\"$_uos_cert_json\"}"
|
||||
|
||||
_create_json="$(_post "$_create_body" "$DEPLOY_UNIFIOS_HOST/api/userCertificates" "" "POST" "application/json")"
|
||||
_create_code="$(_uos_response_code)"
|
||||
|
||||
if [ "$_create_code" = "201" ]; then
|
||||
_new_id="$(echo "$_create_json" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
|
||||
if [ -z "$_new_id" ]; then
|
||||
_err "Could not determine new certificate ID from upload response."
|
||||
return 1
|
||||
fi
|
||||
elif [ "$_create_code" = "400" ] && echo "$_create_json" | grep -q "USER_CERTIFICATE_DUPLICATE"; then
|
||||
# HTTP 400 alone just means "bad request" -- it's the USER_CERTIFICATE_DUPLICATE
|
||||
# code in the response body, checked above, that actually confirms this.
|
||||
# The name above is unique to this run, so a duplicate here can only be
|
||||
# the server's other uniqueness constraint: this exact certificate (by
|
||||
# fingerprint) already exists as some other entry -- most likely a retry
|
||||
# after a prior run already uploaded it (a real renewal always produces a
|
||||
# new fingerprint, so this shouldn't happen in normal cron use). The
|
||||
# response body doesn't include the existing entry's id, so look it up
|
||||
# by fingerprint instead.
|
||||
# The API's own fingerprint field is SHA-1 (20 bytes), not SHA-256 --
|
||||
# confirmed against a real response, e.g.
|
||||
# "fingerprint":"FC:02:50:9C:3B:3F:B7:79:9D:CA:4D:7C:AC:92:E7:D5:EA:F1:3A:29"
|
||||
# (20 colon-separated groups). _fingerprint (core helper) strips the
|
||||
# colons that field has, so re-insert them rather than stripping the
|
||||
# JSON's own colons, which would also remove the ones separating every
|
||||
# key from its value.
|
||||
_uos_fingerprint="$(_fingerprint "$_cfullchain" sha1)"
|
||||
if [ -z "$_uos_fingerprint" ]; then
|
||||
_err "Could not compute the certificate's fingerprint."
|
||||
return 1
|
||||
fi
|
||||
_uos_fingerprint="$(echo "$_uos_fingerprint" | sed 's/\(..\)/\1:/g; s/:$//')"
|
||||
|
||||
_list_json="$(_get "$DEPLOY_UNIFIOS_HOST/api/userCertificates")"
|
||||
_list_code="$(_uos_response_code)"
|
||||
if [ "$_list_code" != "200" ]; then
|
||||
_err "Failed to list existing certificates (HTTP $_list_code)."
|
||||
_err "Response: $_list_json"
|
||||
return 1
|
||||
fi
|
||||
# _normalizeJson collapses the response to one predictable line (no stray
|
||||
# whitespace around colons, no embedded CR/LF the server might emit) but
|
||||
# also strips the trailing newline entirely -- re-terminate before the
|
||||
# split below, since some sed implementations drop an unterminated final
|
||||
# line rather than processing it.
|
||||
_list_json="$(echo "$_list_json" | _normalizeJson)"
|
||||
# A literal embedded newline (not the two-character "\n", which GNU sed
|
||||
# treats as a newline in the replacement but POSIX doesn't define and BSD
|
||||
# sed emits literally) splits it one JSON object per line so grep can
|
||||
# match a single certificate entry at a time.
|
||||
_list_json="$(
|
||||
printf '%s\n' "$_list_json" | sed 's/},{/},\
|
||||
{/g'
|
||||
)"
|
||||
_new_id="$(echo "$_list_json" | grep -F "\"fingerprint\":\"$_uos_fingerprint\"" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
|
||||
if [ -z "$_new_id" ]; then
|
||||
_err "Certificate upload rejected as a duplicate (server reported USER_CERTIFICATE_DUPLICATE), but no existing entry matching this fingerprint was found."
|
||||
_err "Response: $_create_json"
|
||||
return 1
|
||||
fi
|
||||
# Reusing the existing entry rather than deleting it and re-uploading
|
||||
# under today's name+timestamp: the served content is identical either
|
||||
# way, so replacing it would only cost an extra delete+create round trip
|
||||
# for no functional benefit. The tradeoff is cosmetic -- this entry keeps
|
||||
# whatever name it was given whenever it was originally uploaded, so it
|
||||
# won't reflect today's date in the UI.
|
||||
_info "Certificate already present as entry $_new_id; reusing it."
|
||||
else
|
||||
_err "Certificate upload failed (HTTP $_create_code)."
|
||||
_err "Response: $_create_json"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Activating certificate $_new_id..."
|
||||
_activate_json="$(_post '{"active":true}' "$DEPLOY_UNIFIOS_HOST/api/userCertificates/$_new_id/status" "" "PUT" "application/json")"
|
||||
_activate_code="$(_uos_response_code)"
|
||||
|
||||
if [ "$_activate_code" != "200" ]; then
|
||||
_err "Failed to activate new certificate (HTTP $_activate_code)."
|
||||
_err "Response: $_activate_json"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# UniFi OS Server activation is exclusive server-wide. Tests against the
|
||||
# real API confirm this: activation of one entry deactivates whichever
|
||||
# other entry was active before, no matter its name or domain. As a
|
||||
# result, the server serves the certificate that this hook just activated.
|
||||
# This certificate is already live. If the removal of old entries below
|
||||
# fails, the hook logs the failure. The deploy does not fail because of
|
||||
# this.
|
||||
_info "Checking for old certificate entries to remove..."
|
||||
_list_json="$(_get "$DEPLOY_UNIFIOS_HOST/api/userCertificates")"
|
||||
_list_code="$(_uos_response_code)"
|
||||
if [ "$_list_code" != "200" ]; then
|
||||
_err "Failed to list certificates for cleanup (HTTP $_list_code) -- leaving old entries in place."
|
||||
else
|
||||
_list_json="$(echo "$_list_json" | _normalizeJson)"
|
||||
_list_json="$(
|
||||
printf '%s\n' "$_list_json" | sed 's/},{/},\
|
||||
{/g'
|
||||
)"
|
||||
# The pattern below matches the domain name followed by a space. If the
|
||||
# space is missing, the pattern can also match a different domain that
|
||||
# starts with the same text as this domain.
|
||||
_old_ids="$(echo "$_list_json" | grep -F "\"name\":\"$_cdomain " | _egrep_o '"id":"[^"]*"' | cut -d '"' -f 4 | grep -v "^$_new_id$")"
|
||||
for _old_id in $_old_ids; do
|
||||
_info "Removing old certificate entry $_old_id..."
|
||||
_del_json="$(_post "" "$DEPLOY_UNIFIOS_HOST/api/userCertificates/$_old_id" "" "DELETE")"
|
||||
_del_code="$(_uos_response_code)"
|
||||
if [ "$_del_code" != "204" ] && [ "$_del_code" != "200" ]; then
|
||||
_err "Failed to delete old certificate $_old_id (HTTP $_del_code) -- leaving it in place."
|
||||
_err "Response: $_del_json"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
_info "UniFi OS Server certificate deployed and activated successfully."
|
||||
return 0
|
||||
}
|
||||
|
|
@ -80,15 +80,10 @@ vault_deploy() {
|
|||
if [ -n "$VAULT_RENEW_TOKEN" ]; then
|
||||
URL="$VAULT_ADDR/v1/auth/token/renew-self"
|
||||
_info "Renew the Vault token to default TTL"
|
||||
_response=$(_post "" "$URL")
|
||||
if [ "$?" != "0" ]; then
|
||||
if ! _post "" "$URL" >/dev/null; then
|
||||
_err "Failed to renew the Vault token"
|
||||
return 1
|
||||
fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Failed to renew the Vault token: $_response"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
URL="$VAULT_ADDR/v1/$VAULT_PREFIX/$_cdomain"
|
||||
|
|
@ -96,85 +91,29 @@ vault_deploy() {
|
|||
if [ -n "$VAULT_FABIO_MODE" ]; then
|
||||
_info "Writing certificate and key to $URL in Fabio mode"
|
||||
if [ -n "$VAULT_KV_V2" ]; then
|
||||
_response=$(_post "{ \"data\": {\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"} }" "$URL")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error: $_response"
|
||||
return 1
|
||||
fi
|
||||
_post "{ \"data\": {\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"} }" "$URL" >/dev/null || return 1
|
||||
else
|
||||
_response=$(_post "{\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"}" "$URL")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error: $_response"
|
||||
return 1
|
||||
fi
|
||||
_post "{\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"}" "$URL" >/dev/null || return 1
|
||||
fi
|
||||
else
|
||||
if [ -n "$VAULT_KV_V2" ]; then
|
||||
_info "Writing certificate to $URL/cert.pem"
|
||||
_response=$(_post "{\"data\": {\"value\": \"$_ccert\"}}" "$URL/cert.pem")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error writing cert.pem: $_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_post "{\"data\": {\"value\": \"$_ccert\"}}" "$URL/cert.pem" >/dev/null || return 1
|
||||
_info "Writing key to $URL/cert.key"
|
||||
_response=$(_post "{\"data\": {\"value\": \"$_ckey\"}}" "$URL/cert.key")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error writing cert.key: $_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_post "{\"data\": {\"value\": \"$_ckey\"}}" "$URL/cert.key" >/dev/null || return 1
|
||||
_info "Writing CA certificate to $URL/ca.pem"
|
||||
_response=$(_post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/ca.pem")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error writing ca.pem: $_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/ca.pem" >/dev/null || return 1
|
||||
_info "Writing full-chain certificate to $URL/fullchain.pem"
|
||||
_response=$(_post "{\"data\": {\"value\": \"$_cfullchain\"}}" "$URL/fullchain.pem")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error writing fullchain.pem: $_response"
|
||||
return 1
|
||||
fi
|
||||
_post "{\"data\": {\"value\": \"$_cfullchain\"}}" "$URL/fullchain.pem" >/dev/null || return 1
|
||||
else
|
||||
_info "Writing certificate to $URL/cert.pem"
|
||||
_response=$(_post "{\"value\": \"$_ccert\"}" "$URL/cert.pem")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error writing cert.pem: $_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_post "{\"value\": \"$_ccert\"}" "$URL/cert.pem" >/dev/null || return 1
|
||||
_info "Writing key to $URL/cert.key"
|
||||
_response=$(_post "{\"value\": \"$_ckey\"}" "$URL/cert.key")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error writing cert.key: $_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_post "{\"value\": \"$_ckey\"}" "$URL/cert.key" >/dev/null || return 1
|
||||
_info "Writing CA certificate to $URL/ca.pem"
|
||||
_response=$(_post "{\"value\": \"$_cca\"}" "$URL/ca.pem")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error writing ca.pem: $_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_post "{\"value\": \"$_cca\"}" "$URL/ca.pem" >/dev/null || return 1
|
||||
_info "Writing full-chain certificate to $URL/fullchain.pem"
|
||||
_response=$(_post "{\"value\": \"$_cfullchain\"}" "$URL/fullchain.pem")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error writing fullchain.pem: $_response"
|
||||
return 1
|
||||
fi
|
||||
_post "{\"value\": \"$_cfullchain\"}" "$URL/fullchain.pem" >/dev/null || return 1
|
||||
fi
|
||||
|
||||
# To make it compatible with the wrong ca path `chain.pem` which was used in former versions
|
||||
|
|
@ -182,20 +121,11 @@ vault_deploy() {
|
|||
_err "The CA certificate has moved from chain.pem to ca.pem, if you don't depend on chain.pem anymore, you can delete it to avoid this warning"
|
||||
_info "Updating CA certificate to $URL/chain.pem for backward compatibility"
|
||||
if [ -n "$VAULT_KV_V2" ]; then
|
||||
_response=$(_post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/chain.pem")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error writing chain.pem: $_response"
|
||||
return 1
|
||||
fi
|
||||
_post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/chain.pem" >/dev/null || return 1
|
||||
else
|
||||
_response=$(_post "{\"value\": \"$_cca\"}" "$URL/chain.pem")
|
||||
if [ "$?" != "0" ]; then return 1; fi
|
||||
if echo "$_response" | grep -q '"errors":\['; then
|
||||
_err "Vault error writing chain.pem: $_response"
|
||||
return 1
|
||||
fi
|
||||
_post "{\"value\": \"$_cca\"}" "$URL/chain.pem" >/dev/null || return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,5 +106,5 @@ vsftpd_deploy() {
|
|||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,158 +0,0 @@
|
|||
#!/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
|
||||
}
|
||||
|
|
@ -1,500 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Deploy certificates to Zyxel GS1900 series switches
|
||||
#
|
||||
# This script uses the https web administration interface in order
|
||||
# to upload updated certificates to Zyxel GS1900 series switches.
|
||||
# Only a few models have been tested but untested switches from the
|
||||
# same model line may work as well. If you test and confirm a switch
|
||||
# as working please submit a pull request updating this compatibility
|
||||
# list!
|
||||
#
|
||||
# Known Issues:
|
||||
# 1. This is a consumer grade switch and is a bit underpowered
|
||||
# the longer the RSA key size the slower your switch web UI
|
||||
# will be. RSA 2048 will work, RSA 4096 will work but you may
|
||||
# experience performance problems.
|
||||
# 2. You must use RSA certificates. The switch will reject EC-256
|
||||
# and EC-384 certificates in firmware 2.80
|
||||
# See: https://community.zyxel.com/en/discussion/21506/bug-cannot-import-ssl-cert-on-gs1900-8-and-gs1900-24e-firmware-v2-80/
|
||||
#
|
||||
# Current GS1900 Switch Compatibility:
|
||||
# GS1900-8 - Working as of firmware V2.80
|
||||
# GS1900-8HP - Untested
|
||||
# GS1900-10HP - Untested
|
||||
# GS1900-16 - Untested
|
||||
# GS1900-24 - Untested
|
||||
# GS1900-24E - Working as of firmware V2.80
|
||||
# GS1900-24EP - Untested
|
||||
# GS1900-24HP - Untested
|
||||
# GS1900-48 - Untested
|
||||
# GS1900-48HP - Untested
|
||||
#
|
||||
# Prerequisite Setup Steps:
|
||||
# 1. Install at least firmware V2.80 on your switch
|
||||
# 2. Enable HTTPS web management on your switch
|
||||
#
|
||||
# Usage:
|
||||
# 1. Ensure the switch has firmware V2.80 or later.
|
||||
# 2. Ensure the switch has HTTPS management enabled.
|
||||
# 3. Set the appropriate environment variables for your environment.
|
||||
#
|
||||
# DEPLOY_ZYXEL_SWITCH - The switch hostname. (Default: _cdomain)
|
||||
# DEPLOY_ZYXEL_SWITCH_USER - The webadmin user. (Default: admin)
|
||||
# DEPLOY_ZYXEL_SWITCH_PASSWORD - The webadmin password for the switch.
|
||||
# DEPLOY_ZYXEL_SWITCH_REBOOT - If "1" reboot after update. (Default: "0")
|
||||
#
|
||||
# 4. Run the deployment plugin:
|
||||
# acme.sh --deploy --deploy-hook zyxel_gs1900 -d example.com
|
||||
#
|
||||
# returns 0 means success, otherwise error.
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
zyxel_gs1900_deploy() {
|
||||
_zyxel_gs1900_minimum_firmware_version="v2.80"
|
||||
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug2 _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
_getdeployconf DEPLOY_ZYXEL_SWITCH
|
||||
_getdeployconf DEPLOY_ZYXEL_SWITCH_USER
|
||||
_getdeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD
|
||||
_getdeployconf DEPLOY_ZYXEL_SWITCH_REBOOT
|
||||
|
||||
if [ -z "$DEPLOY_ZYXEL_SWITCH" ]; then
|
||||
DEPLOY_ZYXEL_SWITCH="$_cdomain"
|
||||
fi
|
||||
|
||||
if [ -z "$DEPLOY_ZYXEL_SWITCH_USER" ]; then
|
||||
DEPLOY_ZYXEL_SWITCH_USER="admin"
|
||||
fi
|
||||
|
||||
if [ -z "$DEPLOY_ZYXEL_SWITCH_PASSWORD" ]; then
|
||||
DEPLOY_ZYXEL_SWITCH_PASSWORD="1234"
|
||||
fi
|
||||
|
||||
if [ -z "$DEPLOY_ZYXEL_SWITCH_REBOOT" ]; then
|
||||
DEPLOY_ZYXEL_SWITCH_REBOOT="0"
|
||||
fi
|
||||
|
||||
_savedeployconf DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH"
|
||||
_savedeployconf DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER"
|
||||
_savedeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD"
|
||||
_savedeployconf DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT"
|
||||
|
||||
_debug DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH"
|
||||
_debug DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER"
|
||||
_secure_debug DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD"
|
||||
_debug DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT"
|
||||
|
||||
_zyxel_switch_base_uri="https://${DEPLOY_ZYXEL_SWITCH}"
|
||||
|
||||
_info "Beginning to deploy to a Zyxel GS1900 series switch at ${_zyxel_switch_base_uri}."
|
||||
_zyxel_gs1900_deployment_precheck || return $?
|
||||
|
||||
_zyxel_gs1900_should_update
|
||||
if [ "$?" != "0" ]; then
|
||||
_info "The switch already has our certificate installed. No update required."
|
||||
return 0
|
||||
else
|
||||
_info "The switch does not yet have our certificate installed."
|
||||
fi
|
||||
|
||||
_info "Logging into the switch web interface."
|
||||
_zyxel_gs1900_login || return $?
|
||||
|
||||
_info "Validating the switch is compatible with this deployment process."
|
||||
_zyxel_gs1900_validate_device_compatibility || return $?
|
||||
|
||||
_info "Uploading the certificate."
|
||||
_zyxel_gs1900_upload_certificate || return $?
|
||||
|
||||
if [ "$DEPLOY_ZYXEL_SWITCH_REBOOT" = "1" ]; then
|
||||
_info "Rebooting the switch."
|
||||
_zyxel_gs1900_trigger_reboot || return $?
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_zyxel_gs1900_deployment_precheck() {
|
||||
# Initialize the keylength if it isn't already
|
||||
if [ -z "$Le_Keylength" ]; then
|
||||
Le_Keylength=""
|
||||
fi
|
||||
|
||||
if _isEccKey "$Le_Keylength"; then
|
||||
_info "Warning: Zyxel GS1900 switches are not currently known to work with ECC keys!"
|
||||
_info "You can continue, but your switch may reject your key."
|
||||
elif [ -n "$Le_Keylength" ] && [ "$Le_Keylength" -gt "2048" ]; then
|
||||
_info "Warning: Your RSA key length is greater than 2048!"
|
||||
_info "You can continue, but you may experience performance issues in the web administration interface."
|
||||
fi
|
||||
|
||||
# Check the server for some common failure modes prior to authentication and certificate upload in order to avoid
|
||||
# sending a certificate when we may not want to.
|
||||
test_login_response=$(_post "username=test&password=test&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" 2>&1)
|
||||
test_login_page_exitcode="$?"
|
||||
_debug3 "Test Login Response: ${test_login_response}"
|
||||
if [ "$test_login_page_exitcode" -ne "0" ]; then
|
||||
if { [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "60" ]; } || { [ "${ACME_USE_WGET:-0}" = "1" ] && [ "$test_login_page_exitcode" = "5" ]; }; then
|
||||
_err "The SSL certificate at $_zyxel_switch_base_uri could not be validated."
|
||||
_err "Please double check your hostname, port, and that you are actually connecting to your switch."
|
||||
_err "If the problem persists then please ensure that the certificate is not self-signed, has not"
|
||||
_err "expired, and matches the switch hostname. If you expect validation to fail then you can disable"
|
||||
_err "certificate validation by running with --insecure."
|
||||
return 1
|
||||
elif [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "56" ]; then
|
||||
_debug3 "Intentionally ignore curl exit code 56 in our precheck"
|
||||
else
|
||||
_err "Failed to submit the initial login attempt to $_zyxel_switch_base_uri."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_zyxel_gs1900_login() {
|
||||
# Login to the switch and set the appropriate auth cookie in _H1
|
||||
username_encoded=$(printf "%s" "$DEPLOY_ZYXEL_SWITCH_USER" | _url_encode)
|
||||
password_encoded=$(_zyxel_gs1900_password_obfuscate "$DEPLOY_ZYXEL_SWITCH_PASSWORD" | _url_encode)
|
||||
|
||||
login_response=$(_post "username=${username_encoded}&password=${password_encoded}&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n')
|
||||
auth_response=$(_post "authId=${login_response}&login_chk=true" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n')
|
||||
if [ "$auth_response" != "OK" ]; then
|
||||
_err "Login failed due to invalid credentials."
|
||||
_err "Please double check the configured username and password and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
sessionid=$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'HTTPS_XSSID=[^;]*;' | tr -d ';')
|
||||
_secure_debug2 "sessionid" "$sessionid"
|
||||
|
||||
export _H1="Cookie: $sessionid"
|
||||
_secure_debug2 "_H1" "$_H1"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_zyxel_gs1900_validate_device_compatibility() {
|
||||
# Check the switches model and firmware version and throw errors
|
||||
# if this script isn't compatible.
|
||||
device_info_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=12" | tr -d '\n')
|
||||
|
||||
model_name=$(_zyxel_gs1900_get_model "$device_info_html")
|
||||
_debug2 "model_name" "$model_name"
|
||||
if [ -z "$model_name" ]; then
|
||||
_err "Could not find the switch model name."
|
||||
_err "Please re-run with --debug and report a bug."
|
||||
return $?
|
||||
fi
|
||||
|
||||
if ! expr "$model_name" : "GS1900-" >/dev/null; then
|
||||
_err "Switch is an unsupported model: $model_name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
firmware_version=$(_zyxel_gs1900_get_firmware_version "$device_info_html")
|
||||
_debug2 "firmware_version" "$firmware_version"
|
||||
if [ -z "$firmware_version" ]; then
|
||||
_err "Could not find the switch firmware version."
|
||||
_err "Please re-run with --debug and report a bug."
|
||||
return $?
|
||||
fi
|
||||
|
||||
_debug2 "_zyxel_gs1900_minimum_firmware_version" "$_zyxel_gs1900_minimum_firmware_version"
|
||||
minimum_major_version=$(_zyxel_gs1900_parse_major_version "$_zyxel_gs1900_minimum_firmware_version")
|
||||
_debug2 "minimum_major_version" "$minimum_major_version"
|
||||
minimum_minor_version=$(_zyxel_gs1900_parse_minor_version "$_zyxel_gs1900_minimum_firmware_version")
|
||||
_debug2 "minimum_minor_version" "$minimum_minor_version"
|
||||
|
||||
_debug2 "firmware_version" "$firmware_version"
|
||||
firmware_major_version=$(_zyxel_gs1900_parse_major_version "$firmware_version")
|
||||
_debug2 "firmware_major_version" "$firmware_major_version"
|
||||
firmware_minor_version=$(_zyxel_gs1900_parse_minor_version "$firmware_version")
|
||||
_debug2 "firmware_minor_version" "$firmware_minor_version"
|
||||
|
||||
_ret=0
|
||||
if [ "$firmware_major_version" -lt "$minimum_major_version" ]; then
|
||||
_ret=1
|
||||
elif [ "$firmware_major_version" -eq "$minimum_major_version" ] && [ "$firmware_minor_version" -lt "$minimum_minor_version" ]; then
|
||||
_ret=1
|
||||
fi
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Unsupported firmware version $firmware_version. Please upgrade to at least version $_zyxel_gs1900_minimum_firmware_version."
|
||||
fi
|
||||
|
||||
return $?
|
||||
}
|
||||
|
||||
_zyxel_gs1900_should_update() {
|
||||
# Get the remote certificate serial number
|
||||
_remote_cert=$(${ACME_OPENSSL_BIN:-openssl} s_client -showcerts -connect "${DEPLOY_ZYXEL_SWITCH}:443" 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p')
|
||||
_debug3 "_remote_cert" "$_remote_cert"
|
||||
|
||||
_remote_cert_serial=$(printf "%s" "${_remote_cert}" | ${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial)
|
||||
_debug2 "_remote_cert_serial" "$_remote_cert_serial"
|
||||
|
||||
# Get our certificate serial number
|
||||
_our_cert_serial=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial <"${_ccert}")
|
||||
_debug2 "_our_cert_serial" "$_our_cert_serial"
|
||||
|
||||
[ "${_remote_cert_serial}" != "${_our_cert_serial}" ]
|
||||
}
|
||||
|
||||
_zyxel_gs1900_upload_certificate() {
|
||||
# Generate a PKCS12 certificate with a temporary password since the web interface
|
||||
# requires a password be present. Then upload that certificate.
|
||||
temp_cert_password=$(head /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 64)
|
||||
_secure_debug2 "temp_cert_password" "$temp_cert_password"
|
||||
|
||||
temp_pkcs12="$(_mktemp)"
|
||||
_debug2 "temp_pkcs12" "$temp_pkcs12"
|
||||
_toPkcs "$temp_pkcs12" "$_ckey" "$_ccert" "$_cca" "$temp_cert_password"
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Failed to generate a pkcs12 certificate."
|
||||
_err "Please re-run with --debug and report a bug."
|
||||
|
||||
# ensure the temporary certificate file is cleaned up
|
||||
[ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}"
|
||||
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Load the upload page
|
||||
upload_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5914" | tr -d '\n')
|
||||
|
||||
# Get the first instance of XSSID from the upload page
|
||||
form_xss_value=$(printf "%s" "$upload_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g' | head -n 1)
|
||||
_secure_debug2 "form_xss_value" "$form_xss_value"
|
||||
|
||||
_info "Generating the certificate upload request"
|
||||
upload_post_request="$(_mktemp)"
|
||||
upload_post_boundary="---------------------------$(date +%Y%m%d%H%M%S)"
|
||||
|
||||
{
|
||||
printf -- "--%s\r\n" "${upload_post_boundary}"
|
||||
printf "Content-Disposition: form-data; name=\"XSSID\"\r\n\r\n%s\r\n" "${form_xss_value}"
|
||||
printf -- "--%s\r\n" "${upload_post_boundary}"
|
||||
printf "Content-Disposition: form-data; name=\"http_file\"; filename=\"temp_pkcs12.pfx\"\r\n"
|
||||
printf "Content-Type: application/pkcs12\r\n\r\n"
|
||||
cat "${temp_pkcs12}"
|
||||
printf "\r\n"
|
||||
printf -- "--%s\r\n" "${upload_post_boundary}"
|
||||
printf "Content-Disposition: form-data; name=\"pwd\"\r\n\r\n%s\r\n" "${temp_cert_password}"
|
||||
printf -- "--%s\r\n" "${upload_post_boundary}"
|
||||
printf "Content-Disposition: form-data; name=\"cmd\"\r\n\r\n%s\r\n" "31"
|
||||
printf -- "--%s\r\n" "${upload_post_boundary}"
|
||||
printf "Content-Disposition: form-data; name=\"sysSubmit\"\r\n\r\n%s\r\n" "Import"
|
||||
printf -- "--%s--\r\n" "${upload_post_boundary}"
|
||||
} >"${upload_post_request}"
|
||||
|
||||
_info "Upload certificate to the switch"
|
||||
|
||||
# Unfortunately we cannot rely upon the switch response across switch models
|
||||
# to return a consistent body return - so we cannot inspect the result of this
|
||||
# upload to determine success.
|
||||
upload_response=$(_zyxel_upload_pkcs12 "${upload_post_request}" "${upload_post_boundary}" 2>&1)
|
||||
_debug3 "Upload response: ${upload_response}"
|
||||
rm "${upload_post_request}"
|
||||
|
||||
# Pause for a few seconds to give the switch a chance to process the certificate
|
||||
# For some reason I've found this to be necessary on my GS1900-24E
|
||||
_debug2 "Waiting 4 seconds for the switch to process the newly uploaded certificate."
|
||||
sleep "4"
|
||||
|
||||
# Check to see whether or not our update was successful
|
||||
_ret=0
|
||||
_zyxel_gs1900_should_update
|
||||
if [ "$?" != "0" ]; then
|
||||
_info "The certificate was updated successfully"
|
||||
else
|
||||
_ret=1
|
||||
_err "The certificate upload does not appear to have worked."
|
||||
_err "The remote certificate does not match the certificate we tried to upload."
|
||||
_err "Please re-run with --debug 2 and review for unexpected errors. If none can be found please submit a bug."
|
||||
fi
|
||||
|
||||
# ensure the temporary files are cleaned up
|
||||
[ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}"
|
||||
|
||||
return $_ret
|
||||
}
|
||||
|
||||
# make the certificate upload request using either
|
||||
# --data binary with @ for file access in CURL
|
||||
# or using --post-file for wget to ensure we upload
|
||||
# the pkcs12 without getting tripped up on null bytes
|
||||
#
|
||||
# Usage _zyxel_upload_pkcs12 [body file name] [post boundary marker]
|
||||
_zyxel_upload_pkcs12() {
|
||||
bodyfilename="$1"
|
||||
multipartformmarker="$2"
|
||||
_post_url="${_zyxel_switch_base_uri}/cgi-bin/httpuploadcert.cgi"
|
||||
httpmethod="POST"
|
||||
_postContentType="multipart/form-data; boundary=${multipartformmarker}"
|
||||
|
||||
if [ -z "$httpmethod" ]; then
|
||||
httpmethod="POST"
|
||||
fi
|
||||
_debug $httpmethod
|
||||
_debug "_post_url" "$_post_url"
|
||||
_debug2 "bodyfilename" "$bodyfilename"
|
||||
_debug2 "_postContentType" "$_postContentType"
|
||||
|
||||
_inithttp
|
||||
|
||||
if [ "$_ACME_CURL" ] && [ "${ACME_USE_WGET:-0}" = "0" ]; then
|
||||
_CURL="$_ACME_CURL"
|
||||
if [ "$HTTPS_INSECURE" ]; then
|
||||
_CURL="$_CURL --insecure "
|
||||
fi
|
||||
if [ "$httpmethod" = "HEAD" ]; then
|
||||
_CURL="$_CURL -I "
|
||||
fi
|
||||
_debug "_CURL" "$_CURL"
|
||||
|
||||
response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data-binary "@${bodyfilename}" "$_post_url")"
|
||||
|
||||
_ret="$?"
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret"
|
||||
if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
|
||||
_err "Here is the curl dump log:"
|
||||
_err "$(cat "$_CURL_DUMP")"
|
||||
fi
|
||||
fi
|
||||
elif [ "$_ACME_WGET" ]; then
|
||||
_WGET="$_ACME_WGET"
|
||||
if [ "$HTTPS_INSECURE" ]; then
|
||||
_WGET="$_WGET --no-check-certificate "
|
||||
fi
|
||||
_debug "_WGET" "$_WGET"
|
||||
|
||||
response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-file="${bodyfilename}" "$_post_url" 2>"$HTTP_HEADER")"
|
||||
|
||||
_ret="$?"
|
||||
if [ "$_ret" = "8" ]; then
|
||||
_ret=0
|
||||
_debug "wget returned 8 as the server returned a 'Bad Request' response. Let's process the response later."
|
||||
fi
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret"
|
||||
fi
|
||||
if _contains "$_WGET" " -d "; then
|
||||
# Demultiplex wget debug output
|
||||
cat "$HTTP_HEADER" >&2
|
||||
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
|
||||
fi
|
||||
# remove leading whitespaces from header to match curl format
|
||||
_sed_i 's/^ //g' "$HTTP_HEADER"
|
||||
else
|
||||
_ret="$?"
|
||||
_err "Neither curl nor wget have been found, cannot make $httpmethod request."
|
||||
fi
|
||||
_debug "_ret" "$_ret"
|
||||
printf "%s" "$response"
|
||||
return $_ret
|
||||
}
|
||||
|
||||
_zyxel_gs1900_trigger_reboot() {
|
||||
# Trigger a reboot via the management reboot page in the web ui
|
||||
reboot_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5888" | tr -d '\n')
|
||||
reboot_xss_value=$(printf "%s" "$reboot_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g')
|
||||
_secure_debug2 "reboot_xss_value" "$reboot_xss_value"
|
||||
|
||||
reboot_response_html=$(_post "XSSID=${reboot_xss_value}&cmd=5889&sysSubmit=Reboot" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi" '' "POST" "application/x-www-form-urlencoded")
|
||||
reboot_message=$(printf "%s" "$reboot_response_html" | tr -d '\t\r\n\v\f' | _egrep_o "Rebooting now...")
|
||||
|
||||
if [ -z "$reboot_message" ]; then
|
||||
_err "Failed to trigger switch reboot!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# password
|
||||
_zyxel_gs1900_password_obfuscate() {
|
||||
# Return the password obfuscated via the same method used by the
|
||||
# switch's web UI login process
|
||||
echo "$1" | awk '{
|
||||
encoded = "";
|
||||
password = $1;
|
||||
allowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
len = length($1);
|
||||
pwi = length($1);
|
||||
|
||||
for (i=1; i <= (321 - pwi); i++)
|
||||
{
|
||||
if (0 == i % 5 && pwi > 0)
|
||||
{
|
||||
encoded = (encoded)(substr(password, pwi--, 1));
|
||||
}
|
||||
else if (i == 123)
|
||||
{
|
||||
if (len < 10)
|
||||
{
|
||||
encoded = (encoded)(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
encoded = (encoded)(int(len / 10));
|
||||
}
|
||||
}
|
||||
else if (i == 289)
|
||||
{
|
||||
encoded = (encoded)(len % 10)
|
||||
}
|
||||
else
|
||||
{
|
||||
encoded = (encoded)(substr(allowed, int(rand() * length(allowed)), 1))
|
||||
}
|
||||
}
|
||||
printf("%s", encoded);
|
||||
}'
|
||||
}
|
||||
|
||||
# html label
|
||||
_zyxel_html_table_lookup() {
|
||||
# Look up a value in the html representing the status page of the switch
|
||||
# when provided with the html of the page and the label (i.e. "Model Name:")
|
||||
html="$1"
|
||||
label=$(printf "%s" "$2" | tr -d ' ')
|
||||
lookup_result=$(printf "%s" "$html" | tr -d "\t\r\n\v\f" | sed 's/<tr>/\n<tr>/g' | sed 's/<td[^>]*>/<td>/g' | tr -d ' ' | grep -i "$label" | sed "s/<tr><td>$label<\/td><td>\([^<]\{1,\}\)<\/td><\/tr>/\1/i")
|
||||
printf "%s" "$lookup_result"
|
||||
return 0
|
||||
}
|
||||
|
||||
# html
|
||||
_zyxel_gs1900_get_model() {
|
||||
html="$1"
|
||||
model_name=$(_zyxel_html_table_lookup "$html" "Model Name:")
|
||||
printf "%s" "$model_name"
|
||||
}
|
||||
|
||||
# html
|
||||
_zyxel_gs1900_get_firmware_version() {
|
||||
html="$1"
|
||||
firmware_version=$(_zyxel_html_table_lookup "$html" "Firmware Version:" | _egrep_o "V[^.]+.[^(]+")
|
||||
printf "%s" "$firmware_version"
|
||||
}
|
||||
|
||||
# version_number
|
||||
_zyxel_gs1900_parse_major_version() {
|
||||
printf "%s" "$1" | sed 's/^V\([0-9]\{1,\}\).\{1,\}$/\1/gi'
|
||||
}
|
||||
|
||||
# version_number
|
||||
_zyxel_gs1900_parse_minor_version() {
|
||||
printf "%s" "$1" | sed 's/^.\{1,\}\.\([0-9]\{1,\}\)$/\1/gi'
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ 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
|
||||
'
|
||||
|
|
@ -125,28 +124,11 @@ _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/"
|
||||
|
||||
# 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 ';')"
|
||||
_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 'sessionid=[^;]*;' | tr -d ';')"
|
||||
|
||||
if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
|
||||
_err "One or more cookies are empty: '$csrftoken', '$sessionid'."
|
||||
|
|
@ -158,23 +140,17 @@ _1984hosting_login() {
|
|||
csrf_header=$(echo "$csrftoken" | sed 's/csrftoken=//' | _head_n 1)
|
||||
export _H3="X-CSRFToken: $csrf_header"
|
||||
|
||||
url="https://1984.hosting/api/auth/"
|
||||
response="$(_post "username=$username&password=$password&otpkey=$otpkey" "$url")"
|
||||
response="$(_post "username=$username&password=$password&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=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
|
||||
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | 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
|
||||
|
|
@ -185,7 +161,6 @@ _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=""
|
||||
|
|
@ -250,15 +225,9 @@ _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"
|
||||
|
|
@ -266,7 +235,6 @@ _get_zone_id() {
|
|||
_err "Error getting _zone_id for $2."
|
||||
return 1
|
||||
fi
|
||||
_zone_id_for="$domain"
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -289,8 +257,9 @@ _htmlget() {
|
|||
|
||||
# Add extra headers to request
|
||||
_authpost() {
|
||||
_get_zone_id "https://1984.hosting/domains" "$_domain"
|
||||
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | sed 's/csrftoken=//' | _head_n 1)"
|
||||
url="https://1984.hosting/domains"
|
||||
_get_zone_id "$url" "$_domain"
|
||||
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")"
|
||||
export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE"
|
||||
export _H2="Referer: https://1984.hosting/domains/$_zone_id"
|
||||
export _H3="X-CSRFToken: $csrf_header"
|
||||
|
|
|
|||
|
|
@ -37,16 +37,6 @@ 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
|
||||
|
|
@ -81,7 +71,7 @@ dns_acmedns_add() {
|
|||
data="{\"subdomain\":\"$ACMEDNS_SUBDOMAIN\", \"txt\": \"$txtvalue\"}"
|
||||
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST" "application/json")"
|
||||
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST")"
|
||||
_debug response "$response"
|
||||
|
||||
if ! echo "$response" | grep "\"$txtvalue\"" >/dev/null; then
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_active24_info='Active24.cz
|
||||
Site: Active24.cz
|
||||
dns_active24_info='Active24.com
|
||||
Site: Active24.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_active24
|
||||
Options:
|
||||
Active24_ApiKey API Key. Called "Identifier" in the Active24 Admin
|
||||
Active24_ApiSecret API Secret. Called "Secret key" in the Active24 Admin
|
||||
ACTIVE24_Token API Token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2059
|
||||
Author: Milan Pála
|
||||
'
|
||||
|
||||
Active24_Api="https://rest.active24.cz"
|
||||
# export Active24_ApiKey=ak48l3h7-ak5d-qn4t-p8gc-b6fs8c3l
|
||||
# export Active24_ApiSecret=ajvkeo3y82ndsu2smvxy3o36496dcascksldncsq
|
||||
ACTIVE24_Api="https://api.active24.com"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
# Used to add txt record
|
||||
|
|
@ -22,8 +22,8 @@ dns_active24_add() {
|
|||
_active24_init
|
||||
|
||||
_info "Adding txt record"
|
||||
if _active24_rest POST "/v2/service/$_service_id/dns/record" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":300}"; then
|
||||
if _contains "$response" "error"; then
|
||||
if _active24_rest POST "dns/$_domain/txt/v1" "{\"name\":\"$_sub_domain\",\"text\":\"$txtvalue\",\"ttl\":0}"; then
|
||||
if _contains "$response" "errors"; then
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
else
|
||||
|
|
@ -31,7 +31,6 @@ dns_active24_add() {
|
|||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
}
|
||||
|
|
@ -45,25 +44,19 @@ dns_active24_rm() {
|
|||
_active24_init
|
||||
|
||||
_debug "Getting txt records"
|
||||
# The API needs to send data in body in order the filter to work
|
||||
# TODO: web can also add content $txtvalue to filter and then get the id from response
|
||||
_active24_rest GET "/v2/service/$_service_id/dns/record" "{\"page\":1,\"descending\":true,\"sortBy\":\"name\",\"rowsPerPage\":100,\"totalRecords\":0,\"filters\":{\"type\":[\"TXT\"],\"name\":\"${_sub_domain}\"}}"
|
||||
#_active24_rest GET "/v2/service/$_service_id/dns/record?rowsPerPage=100"
|
||||
_active24_rest GET "dns/$_domain/records/v1"
|
||||
|
||||
if _contains "$response" "error"; then
|
||||
if _contains "$response" "errors"; then
|
||||
_err "Error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Note: it might never be more than one record actually, NEEDS more INVESTIGATION
|
||||
record_ids=$(printf "%s" "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o '"id" *: *[^,]+' | cut -d ':' -f 2)
|
||||
_debug2 record_ids "$record_ids"
|
||||
hash_ids=$(echo "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o "hashId\":\"[^\"]+" | cut -c10-)
|
||||
|
||||
for redord_id in $record_ids; do
|
||||
_debug "Removing record_id" "$redord_id"
|
||||
_debug "txtvalue" "$txtvalue"
|
||||
if _active24_rest DELETE "/v2/service/$_service_id/dns/record/$redord_id" ""; then
|
||||
if _contains "$response" "error"; then
|
||||
for hash_id in $hash_ids; do
|
||||
_debug "Removing hash_id" "$hash_id"
|
||||
if _active24_rest DELETE "dns/$_domain/$hash_id/v1" ""; then
|
||||
if _contains "$response" "errors"; then
|
||||
_err "Unable to remove txt record."
|
||||
return 1
|
||||
else
|
||||
|
|
@ -77,17 +70,23 @@ dns_active24_rm() {
|
|||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
# _domain_id=sdjkglgdfewsdfg
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
if ! _active24_rest GET "/v1/user/self/service"; then
|
||||
if ! _active24_rest GET "dns/domains/v1"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
i=2
|
||||
p=1
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
|
||||
_debug "h" "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
|
|
@ -95,7 +94,7 @@ _get_root() {
|
|||
fi
|
||||
|
||||
if _contains "$response" "\"$h\"" >/dev/null; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
|
@ -105,98 +104,21 @@ _get_root() {
|
|||
return 1
|
||||
}
|
||||
|
||||
_active24_init() {
|
||||
Active24_ApiKey="${Active24_ApiKey:-$(_readaccountconf_mutable Active24_ApiKey)}"
|
||||
Active24_ApiSecret="${Active24_ApiSecret:-$(_readaccountconf_mutable Active24_ApiSecret)}"
|
||||
#Active24_ServiceId="${Active24_ServiceId:-$(_readaccountconf_mutable Active24_ServiceId)}"
|
||||
|
||||
if [ -z "$Active24_ApiKey" ] || [ -z "$Active24_ApiSecret" ]; then
|
||||
Active24_ApiKey=""
|
||||
Active24_ApiSecret=""
|
||||
_err "You don't specify Active24 api key and ApiSecret yet."
|
||||
_err "Please create your key and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
#save the credentials to the account conf file.
|
||||
_saveaccountconf_mutable Active24_ApiKey "$Active24_ApiKey"
|
||||
_saveaccountconf_mutable Active24_ApiSecret "$Active24_ApiSecret"
|
||||
|
||||
_debug "A24 API CHECK"
|
||||
if ! _active24_rest GET "/v2/check"; then
|
||||
_err "A24 API check failed with: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! echo "$response" | tr -d " " | grep \"verified\":true >/dev/null; then
|
||||
_err "A24 API check failed with: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
_active24_get_service_id "$_domain"
|
||||
_debug _service_id "$_service_id"
|
||||
}
|
||||
|
||||
_active24_get_service_id() {
|
||||
_d=$1
|
||||
if ! _active24_rest GET "/v1/user/self/zone/${_d}"; then
|
||||
return 1
|
||||
else
|
||||
response=$(echo "$response" | _json_decode)
|
||||
_service_id=$(echo "$response" | _egrep_o '"id" *: *[^,]+' | cut -d ':' -f 2)
|
||||
fi
|
||||
}
|
||||
|
||||
_active24_rest() {
|
||||
m=$1
|
||||
ep_qs=$2 # with query string
|
||||
# ep=$2
|
||||
ep=$(printf "%s" "$ep_qs" | cut -d '?' -f1) # no query string
|
||||
ep="$2"
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
_debug "A24 $ep"
|
||||
_debug "A24 $Active24_ApiKey"
|
||||
_debug "A24 $Active24_ApiSecret"
|
||||
|
||||
timestamp=$(_time)
|
||||
datez=$(date -u +"%Y%m%dT%H%M%SZ")
|
||||
canonicalRequest="${m} ${ep} ${timestamp}"
|
||||
signature=$(printf "%s" "$canonicalRequest" | _hmac sha1 "$(printf "%s" "$Active24_ApiSecret" | _hex_dump | tr -d " ")" hex)
|
||||
authorization64="$(printf "%s:%s" "$Active24_ApiKey" "$signature" | _base64)"
|
||||
|
||||
export _H1="Date: ${datez}"
|
||||
export _H2="Accept: application/json"
|
||||
export _H3="Content-Type: application/json"
|
||||
export _H4="Authorization: Basic ${authorization64}"
|
||||
|
||||
_debug2 H1 "$_H1"
|
||||
_debug2 H2 "$_H2"
|
||||
_debug2 H3 "$_H3"
|
||||
_debug2 H4 "$_H4"
|
||||
|
||||
# _sleep 1
|
||||
export _H1="Authorization: Bearer $ACTIVE24_Token"
|
||||
|
||||
if [ "$m" != "GET" ]; then
|
||||
_debug2 "${m} $Active24_Api${ep_qs}"
|
||||
_debug "data" "$data"
|
||||
response="$(_post "$data" "$Active24_Api${ep_qs}" "" "$m" "application/json")"
|
||||
response="$(_post "$data" "$ACTIVE24_Api/$ep" "" "$m" "application/json")"
|
||||
else
|
||||
if [ -z "$data" ]; then
|
||||
_debug2 "GET $Active24_Api${ep_qs}"
|
||||
response="$(_get "$Active24_Api${ep_qs}")"
|
||||
else
|
||||
_debug2 "GET $Active24_Api${ep_qs} with data: ${data}"
|
||||
response="$(_post "$data" "$Active24_Api${ep_qs}" "" "$m" "application/json")"
|
||||
fi
|
||||
response="$(_get "$ACTIVE24_Api/$ep")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "error $ep"
|
||||
return 1
|
||||
|
|
@ -204,3 +126,23 @@ _active24_rest() {
|
|||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
|
||||
_active24_init() {
|
||||
ACTIVE24_Token="${ACTIVE24_Token:-$(_readaccountconf_mutable ACTIVE24_Token)}"
|
||||
if [ -z "$ACTIVE24_Token" ]; then
|
||||
ACTIVE24_Token=""
|
||||
_err "You didn't specify a Active24 api token yet."
|
||||
_err "Please create the token and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable ACTIVE24_Token "$ACTIVE24_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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ _get_root() {
|
|||
if _ad_rest GET "domain/"; then
|
||||
response="$(echo "$response" | tr -d "\n" | sed 's/{/\n&/g')"
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
|
|
@ -106,7 +106,7 @@ _get_root() {
|
|||
if [ "$hostedzone" ]; then
|
||||
_domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -9,48 +9,13 @@ Options:
|
|||
Ali_Secret API Secret
|
||||
'
|
||||
|
||||
# NOTICE:
|
||||
# This file is referenced by Alibaba Cloud Services deploy hooks
|
||||
# https://github.com/acmesh-official/acme.sh/pull/5205#issuecomment-2357867276
|
||||
# Be careful when modifying this file, especially when making breaking changes for common functions
|
||||
|
||||
Ali_DNS_API="https://alidns.aliyuncs.com/"
|
||||
Ali_API="https://alidns.aliyuncs.com/"
|
||||
|
||||
#Usage: dns_ali_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_ali_add() {
|
||||
# the API only accepts punycode for IDN domains, and a raw UTF-8 domain
|
||||
# also breaks the request signature (issue 4733)
|
||||
fulldomain=$(_idn "$1")
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
_prepare_ali_credentials || return 1
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Add record"
|
||||
_add_record_query "$_domain" "$_sub_domain" "$txtvalue" && _ali_rest "Add record"
|
||||
}
|
||||
|
||||
dns_ali_rm() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
|
||||
Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_clean
|
||||
}
|
||||
|
||||
#################### Alibaba Cloud common functions below ####################
|
||||
|
||||
_prepare_ali_credentials() {
|
||||
Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
|
||||
Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
|
||||
if [ -z "$Ali_Key" ] || [ -z "$Ali_Secret" ]; then
|
||||
|
|
@ -63,74 +28,38 @@ _prepare_ali_credentials() {
|
|||
#save the api key and secret to the account conf file.
|
||||
_saveaccountconf_mutable Ali_Key "$Ali_Key"
|
||||
_saveaccountconf_mutable Ali_Secret "$Ali_Secret"
|
||||
}
|
||||
|
||||
# act ign mtd
|
||||
_ali_rest() {
|
||||
act="$1"
|
||||
ign="$2"
|
||||
mtd="${3:-GET}"
|
||||
|
||||
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
|
||||
url="$url&$query"
|
||||
response="$(_get "$url")"
|
||||
else
|
||||
response="$(_post "$query" "$url" "" "$mtd" "application/x-www-form-urlencoded")"
|
||||
fi
|
||||
|
||||
_ret="$?"
|
||||
_debug2 response "$response"
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Error <$act>"
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$ign" ]; then
|
||||
message="$(echo "$response" | _egrep_o "\"Message\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")"
|
||||
if [ "$message" ]; then
|
||||
_err "$message"
|
||||
return 1
|
||||
fi
|
||||
_debug "Add record"
|
||||
_add_record_query "$_domain" "$_sub_domain" "$txtvalue" && _ali_rest "Add record"
|
||||
}
|
||||
|
||||
dns_ali_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
|
||||
Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_clean
|
||||
}
|
||||
|
||||
# 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'
|
||||
}
|
||||
|
||||
_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"
|
||||
}
|
||||
|
||||
#################### Private functions below ####################
|
||||
#################### Private functions below ##################################
|
||||
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
i=2
|
||||
p=1
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
return 1
|
||||
|
|
@ -142,7 +71,7 @@ _get_root() {
|
|||
fi
|
||||
|
||||
if _contains "$response" "PageNumber"; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_domain="$h"
|
||||
_debug _domain "$_domain"
|
||||
|
|
@ -154,10 +83,52 @@ _get_root() {
|
|||
return 1
|
||||
}
|
||||
|
||||
_ali_rest() {
|
||||
signature=$(printf "%s" "GET&%2F&$(_ali_urlencode "$query")" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
|
||||
signature=$(_ali_urlencode "$signature")
|
||||
url="$Ali_API?$query&Signature=$signature"
|
||||
|
||||
if ! response="$(_get "$url")"; then
|
||||
_err "Error <$1>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 response "$response"
|
||||
if [ -z "$2" ]; then
|
||||
message="$(echo "$response" | _egrep_o "\"Message\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")"
|
||||
if [ "$message" ]; then
|
||||
_err "$message"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_ali_urlencode() {
|
||||
_str="$1"
|
||||
_str_len=${#_str}
|
||||
_u_i=1
|
||||
while [ "$_u_i" -le "$_str_len" ]; do
|
||||
_str_c="$(printf "%s" "$_str" | cut -c "$_u_i")"
|
||||
case $_str_c in [a-zA-Z0-9.~_-])
|
||||
printf "%s" "$_str_c"
|
||||
;;
|
||||
*)
|
||||
printf "%%%02X" "'$_str_c"
|
||||
;;
|
||||
esac
|
||||
_u_i="$(_math "$_u_i" + 1)"
|
||||
done
|
||||
}
|
||||
|
||||
_ali_nonce() {
|
||||
#_head_n 1 </dev/urandom | _digest "sha256" hex | cut -c 1-31
|
||||
#Not so good...
|
||||
date +"%s%N" | sed 's/%N//g'
|
||||
}
|
||||
|
||||
_check_exist_query() {
|
||||
_qdomain="$1"
|
||||
_qsubdomain="$2"
|
||||
endpoint=$Ali_DNS_API
|
||||
query=''
|
||||
query=$query'AccessKeyId='$Ali_Key
|
||||
query=$query'&Action=DescribeDomainRecords'
|
||||
|
|
@ -167,13 +138,12 @@ _check_exist_query() {
|
|||
query=$query'&SignatureMethod=HMAC-SHA1'
|
||||
query=$query"&SignatureNonce=$(_ali_nonce)"
|
||||
query=$query'&SignatureVersion=1.0'
|
||||
query=$query'&Timestamp='$(_ali_timestamp)
|
||||
query=$query'&Timestamp='$(_timestamp)
|
||||
query=$query'&TypeKeyWord=TXT'
|
||||
query=$query'&Version=2015-01-09'
|
||||
}
|
||||
|
||||
_add_record_query() {
|
||||
endpoint=$Ali_DNS_API
|
||||
query=''
|
||||
query=$query'AccessKeyId='$Ali_Key
|
||||
query=$query'&Action=AddDomainRecord'
|
||||
|
|
@ -183,14 +153,13 @@ _add_record_query() {
|
|||
query=$query'&SignatureMethod=HMAC-SHA1'
|
||||
query=$query"&SignatureNonce=$(_ali_nonce)"
|
||||
query=$query'&SignatureVersion=1.0'
|
||||
query=$query'&Timestamp='$(_ali_timestamp)
|
||||
query=$query'&Timestamp='$(_timestamp)
|
||||
query=$query'&Type=TXT'
|
||||
query=$query'&Value='$3
|
||||
query=$query'&Version=2015-01-09'
|
||||
}
|
||||
|
||||
_delete_record_query() {
|
||||
endpoint=$Ali_DNS_API
|
||||
query=''
|
||||
query=$query'AccessKeyId='$Ali_Key
|
||||
query=$query'&Action=DeleteDomainRecord'
|
||||
|
|
@ -199,12 +168,11 @@ _delete_record_query() {
|
|||
query=$query'&SignatureMethod=HMAC-SHA1'
|
||||
query=$query"&SignatureNonce=$(_ali_nonce)"
|
||||
query=$query'&SignatureVersion=1.0'
|
||||
query=$query'&Timestamp='$(_ali_timestamp)
|
||||
query=$query'&Timestamp='$(_timestamp)
|
||||
query=$query'&Version=2015-01-09'
|
||||
}
|
||||
|
||||
_describe_records_query() {
|
||||
endpoint=$Ali_DNS_API
|
||||
query=''
|
||||
query=$query'AccessKeyId='$Ali_Key
|
||||
query=$query'&Action=DescribeDomainRecords'
|
||||
|
|
@ -213,7 +181,7 @@ _describe_records_query() {
|
|||
query=$query'&SignatureMethod=HMAC-SHA1'
|
||||
query=$query"&SignatureNonce=$(_ali_nonce)"
|
||||
query=$query'&SignatureVersion=1.0'
|
||||
query=$query'&Timestamp='$(_ali_timestamp)
|
||||
query=$query'&Timestamp='$(_timestamp)
|
||||
query=$query'&Version=2015-01-09'
|
||||
}
|
||||
|
||||
|
|
@ -235,3 +203,7 @@ _clean() {
|
|||
fi
|
||||
|
||||
}
|
||||
|
||||
_timestamp() {
|
||||
date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_alviy_info='Alviy.com
|
||||
Site: Alviy.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_alviy
|
||||
Options:
|
||||
Alviy_token API token. Get it from the https://cloud.alviy.com/token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/5115
|
||||
'
|
||||
# Alviy domain api
|
||||
#
|
||||
# Get API key and secret from https://cloud.alviy.com/token
|
||||
#
|
||||
# Alviy_token="some-secret-key"
|
||||
#
|
||||
# Ex.: acme.sh --issue --staging --dns dns_alviy -d "*.s.example.com" -d "s.example.com"
|
||||
|
||||
Alviy_Api="https://cloud.alviy.com/api/v1"
|
||||
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ _get_root() {
|
|||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
|
|
@ -140,7 +140,7 @@ _get_root() {
|
|||
|
||||
_anx_rest GET "zone.json/${h}"
|
||||
if _contains "$response" "\"name\":\"$h\""; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,490 +0,0 @@
|
|||
#!/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
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ _get_root() {
|
|||
i=2
|
||||
p=1
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
|
|
@ -120,7 +120,7 @@ _get_root() {
|
|||
if _contains "$response" "\"domain\":\"$h\""; then
|
||||
_domain_id=$(echo "$response" | cut -d : -f 3 | cut -d , -f 1 | tr -d \")
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ _get_root() {
|
|||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
|
|
@ -132,7 +132,7 @@ _get_root() {
|
|||
_domain_id=$(echo "$response" | _normalizeJson | tr -d "{}" | tr "," "\n" | grep "\"id\": *\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||
_debug _domain_id "$_domain_id"
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ _get_autodns_zone() {
|
|||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
|
||||
_debug h "$h"
|
||||
|
||||
if [ -z "$h" ]; then
|
||||
|
|
@ -128,7 +128,7 @@ _get_autodns_zone() {
|
|||
if _contains "$autodns_response" "<summary>1</summary>" >/dev/null; then
|
||||
_zone="$(echo "$autodns_response" | _egrep_o '<name>[^<]*</name>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
|
||||
_system_ns="$(echo "$autodns_response" | _egrep_o '<system_ns>[^<]*</system_ns>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
|
@ -139,21 +139,12 @@ _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_xml" "$_autodns_password_xml" "$_autodns_context_xml"
|
||||
</auth>" "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT"
|
||||
}
|
||||
|
||||
# Arguments:
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ Options:
|
|||
# All `_sleep` commands are included to avoid Route53 throttling, see
|
||||
# https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests
|
||||
|
||||
# Updated from "route53.amazonaws.com"
|
||||
AWS_HOST="route53.global.api.aws"
|
||||
AWS_HOST="route53.amazonaws.com"
|
||||
AWS_URL="https://$AWS_HOST"
|
||||
|
||||
AWS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Amazon-Route53-API"
|
||||
|
|
@ -159,10 +158,10 @@ _get_root() {
|
|||
|
||||
# iterate over names (a.b.c.d -> b.c.d -> c.d -> d)
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100 | sed 's/\./\\./g')
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100 | sed 's/\./\\./g')
|
||||
_debug "Checking domain: $h"
|
||||
if [ -z "$h" ]; then
|
||||
_err "invalid domain"
|
||||
_error "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
|
@ -175,7 +174,7 @@ _get_root() {
|
|||
if [ "$hostedzone" ]; then
|
||||
_domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "<Id>.*<.Id>" | head -n 1 | _egrep_o ">.*<" | tr -d "<>")
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ _get_root() {
|
|||
fi
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
# not valid
|
||||
|
|
@ -111,7 +111,7 @@ _get_root() {
|
|||
_domain_id=$(echo "$response" | tr '{' "\n" | grep "\"domain\":\"$h\"" | _egrep_o "\"id\":[0-9]*" | _head_n 1 | cut -d : -f 2 | tr -d \")
|
||||
_debug _domain_id "$_domain_id"
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -9,17 +9,14 @@ Options:
|
|||
AZUREDNS_APPID App ID. App ID of the service principal
|
||||
AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal
|
||||
AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false"
|
||||
AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Optional.
|
||||
'
|
||||
|
||||
wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
# Used to add txt record
|
||||
#
|
||||
# Ref: https://learn.microsoft.com/en-us/rest/api/dns/record-sets/create-or-update?view=rest-dns-2018-05-01&tabs=HTTP
|
||||
# Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/createorupdate
|
||||
#
|
||||
|
||||
dns_azure_add() {
|
||||
|
|
@ -32,7 +29,6 @@ dns_azure_add() {
|
|||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
AZUREDNS_BEARERTOKEN=""
|
||||
_err "You didn't specify the Azure Subscription ID"
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -47,45 +43,37 @@ dns_azure_add() {
|
|||
_saveaccountconf_mutable AZUREDNS_TENANTID ""
|
||||
_saveaccountconf_mutable AZUREDNS_APPID ""
|
||||
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET ""
|
||||
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN ""
|
||||
else
|
||||
_info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token"
|
||||
_info "You didn't ask to use Azure managed identity, checking service principal credentials"
|
||||
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
|
||||
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
|
||||
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
|
||||
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
|
||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||
if [ -z "$AZUREDNS_TENANTID" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
AZUREDNS_BEARERTOKEN=""
|
||||
_err "You didn't specify the Azure Tenant ID "
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$AZUREDNS_APPID" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
AZUREDNS_BEARERTOKEN=""
|
||||
_err "You didn't specify the Azure App ID"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$AZUREDNS_TENANTID" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
_err "You didn't specify the Azure Tenant ID "
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
AZUREDNS_BEARERTOKEN=""
|
||||
_err "You didn't specify the Azure Client Secret"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
_info "Using provided bearer token"
|
||||
if [ -z "$AZUREDNS_APPID" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
_err "You didn't specify the Azure App ID"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
_err "You didn't specify the Azure Client Secret"
|
||||
return 1
|
||||
fi
|
||||
|
||||
#save account details to account conf file, don't opt in for azure manages identity check.
|
||||
|
|
@ -93,14 +81,9 @@ dns_azure_add() {
|
|||
_saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
|
||||
_saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
|
||||
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
|
||||
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$AZUREDNS_BEARERTOKEN"
|
||||
fi
|
||||
|
||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||
accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
|
||||
else
|
||||
accesstoken=$(echo "$AZUREDNS_BEARERTOKEN" | sed "s/Bearer //g")
|
||||
fi
|
||||
accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
|
||||
|
||||
if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then
|
||||
_err "invalid domain"
|
||||
|
|
@ -150,7 +133,7 @@ dns_azure_add() {
|
|||
# Usage: fulldomain txtvalue
|
||||
# Used to remove the txt record after validation
|
||||
#
|
||||
# Ref: https://learn.microsoft.com/en-us/rest/api/dns/record-sets/delete?view=rest-dns-2018-05-01&tabs=HTTP
|
||||
# Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/delete
|
||||
#
|
||||
dns_azure_rm() {
|
||||
fulldomain=$1
|
||||
|
|
@ -162,7 +145,6 @@ dns_azure_rm() {
|
|||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
AZUREDNS_BEARERTOKEN=""
|
||||
_err "You didn't specify the Azure Subscription ID "
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -171,51 +153,40 @@ dns_azure_rm() {
|
|||
if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then
|
||||
_info "Using Azure managed identity"
|
||||
else
|
||||
_info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token"
|
||||
_info "You didn't ask to use Azure managed identity, checking service principal credentials"
|
||||
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
|
||||
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
|
||||
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
|
||||
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
|
||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||
if [ -z "$AZUREDNS_TENANTID" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
AZUREDNS_BEARERTOKEN=""
|
||||
_err "You didn't specify the Azure Tenant ID "
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$AZUREDNS_APPID" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
AZUREDNS_BEARERTOKEN=""
|
||||
_err "You didn't specify the Azure App ID"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$AZUREDNS_TENANTID" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
_err "You didn't specify the Azure Tenant ID "
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
AZUREDNS_BEARERTOKEN=""
|
||||
_err "You didn't specify the Azure Client Secret"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
_info "Using provided bearer token"
|
||||
if [ -z "$AZUREDNS_APPID" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
_err "You didn't specify the Azure App ID"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
|
||||
AZUREDNS_SUBSCRIPTIONID=""
|
||||
AZUREDNS_TENANTID=""
|
||||
AZUREDNS_APPID=""
|
||||
AZUREDNS_CLIENTSECRET=""
|
||||
_err "You didn't specify the Azure Client Secret"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||
accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
|
||||
else
|
||||
accesstoken=$(echo "$AZUREDNS_BEARERTOKEN" | sed "s/Bearer //g")
|
||||
fi
|
||||
accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
|
||||
|
||||
if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then
|
||||
_err "invalid domain"
|
||||
|
|
@ -294,10 +265,10 @@ _azure_rest() {
|
|||
if [ "$_code" = "401" ]; then
|
||||
# we have an invalid access token set to expired
|
||||
_saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "0"
|
||||
_err "Access denied. Invalid access token. Make sure your Azure settings are correct. See: $wiki"
|
||||
_err "access denied make sure your Azure settings are correct. See $WIKI"
|
||||
return 1
|
||||
fi
|
||||
# See https://learn.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific#general-rest-and-retry-guidelines for retryable HTTP codes
|
||||
# See https://docs.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific#general-rest-and-retry-guidelines for retryable HTTP codes
|
||||
if [ "$_ret" != "0" ] || [ -z "$_code" ] || [ "$_code" = "408" ] || [ "$_code" = "500" ] || [ "$_code" = "503" ] || [ "$_code" = "504" ]; then
|
||||
_request_retry_times="$(_math "$_request_retry_times" + 1)"
|
||||
_info "REST call error $_code retrying $ep in $_request_retry_times s"
|
||||
|
|
@ -315,14 +286,14 @@ _azure_rest() {
|
|||
return 0
|
||||
}
|
||||
|
||||
## Ref: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#request-an-access-token
|
||||
## Ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-service-to-service#request-an-access-token
|
||||
_azure_getaccess_token() {
|
||||
managedIdentity=$1
|
||||
tenantID=$2
|
||||
clientID=$3
|
||||
clientSecret=$4
|
||||
|
||||
accesstoken="${AZUREDNS_ACCESSTOKEN:-$(_readaccountconf_mutable AZUREDNS_ACCESSTOKEN)}"
|
||||
accesstoken="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
|
||||
expires_on="${AZUREDNS_TOKENVALIDTO:-$(_readaccountconf_mutable AZUREDNS_TOKENVALIDTO)}"
|
||||
|
||||
# can we reuse the bearer token?
|
||||
|
|
@ -339,18 +310,9 @@ _azure_getaccess_token() {
|
|||
_debug "getting new bearer token"
|
||||
|
||||
if [ "$managedIdentity" = true ]; then
|
||||
# https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
|
||||
if [ -n "$IDENTITY_ENDPOINT" ]; then
|
||||
# Some Azure environments may set IDENTITY_ENDPOINT (formerly MSI_ENDPOINT) to have an alternative metadata endpoint
|
||||
url="$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=https://management.azure.com/"
|
||||
headers="X-IDENTITY-HEADER: $IDENTITY_HEADER"
|
||||
else
|
||||
url="http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
|
||||
headers="Metadata: true"
|
||||
fi
|
||||
|
||||
export _H1="$headers"
|
||||
response="$(_get "$url")"
|
||||
# https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
|
||||
export _H1="Metadata: true"
|
||||
response="$(_get http://169.254.169.254/metadata/identity/oauth2/token\?api-version=2018-02-01\&resource=https://management.azure.com/)"
|
||||
response="$(echo "$response" | _normalizeJson)"
|
||||
accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
|
||||
expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
|
||||
|
|
@ -368,14 +330,14 @@ _azure_getaccess_token() {
|
|||
fi
|
||||
|
||||
if [ -z "$accesstoken" ]; then
|
||||
_err "No acccess token received. Check your Azure settings. See: $wiki"
|
||||
_err "no acccess token received. Check your Azure settings see $WIKI"
|
||||
return 1
|
||||
fi
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "error $response"
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable AZUREDNS_ACCESSTOKEN "$accesstoken"
|
||||
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$accesstoken"
|
||||
_saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "$expires_on"
|
||||
printf "%s" "$accesstoken"
|
||||
return 0
|
||||
|
|
@ -388,18 +350,15 @@ _get_root() {
|
|||
i=1
|
||||
p=1
|
||||
|
||||
## Ref: https://learn.microsoft.com/en-us/rest/api/dns/zones/list?view=rest-dns-2018-05-01&tabs=HTTP
|
||||
## returns up to 100 zones in one response. Handling more results is not implemented
|
||||
## (ZoneListResult with continuation token for the next page of results)
|
||||
##
|
||||
## TODO: handle more than 100 results, as per:
|
||||
## https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-dns-limits
|
||||
## The new limit is 250 Public DNS zones per subscription, while the old limit was only 100
|
||||
## Ref: https://docs.microsoft.com/en-us/rest/api/dns/zones/list
|
||||
## returns up to 100 zones in one response therefore handling more results is not not implemented
|
||||
## (ZoneListResult with continuation token for the next page of results)
|
||||
## Per https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#dns-limits you are limited to 100 Zone/subscriptions anyways
|
||||
##
|
||||
_azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?\$top=500&api-version=2017-09-01" "" "$accesstoken"
|
||||
# Find matching domain name in Json response
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
|
||||
_debug2 "Checking domain: $h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
|
|
@ -414,7 +373,7 @@ _get_root() {
|
|||
#create the record at the domain apex (@) if only the domain name was provided as --domain-alias
|
||||
_sub_domain="@"
|
||||
else
|
||||
_sub_domain=$(echo "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(echo "$domain" | cut -d . -f 1-$p)
|
||||
fi
|
||||
_domain=$h
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -1,775 +0,0 @@
|
|||
#!/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
|
||||
}
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_beget_info='Beget.com
|
||||
Site: Beget.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_beget
|
||||
Options:
|
||||
BEGET_User API user
|
||||
BEGET_Password API password
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6200
|
||||
Author: ARNik <arnik@arnik.ru>
|
||||
'
|
||||
|
||||
Beget_Api="https://api.beget.com/api"
|
||||
|
||||
#################### Public functions ####################
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
# Used to add txt record
|
||||
dns_beget_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
_debug "dns_beget_add() $fulldomain $txtvalue"
|
||||
fulldomain=$(echo "$fulldomain" | _lower_case)
|
||||
|
||||
Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}"
|
||||
Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}"
|
||||
|
||||
if [ -z "$Beget_Username" ] || [ -z "$Beget_Password" ]; then
|
||||
Beget_Username=""
|
||||
Beget_Password=""
|
||||
_err "You must export variables: Beget_Username, and Beget_Password"
|
||||
return 1
|
||||
fi
|
||||
|
||||
#save the credentials to the account conf file.
|
||||
_saveaccountconf_mutable Beget_Username "$Beget_Username"
|
||||
_saveaccountconf_mutable Beget_Password "$Beget_Password"
|
||||
|
||||
_info "Prepare subdomain."
|
||||
if ! _prepare_subdomain "$fulldomain"; then
|
||||
_err "Can't prepare subdomain."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Get domain records"
|
||||
data="{\"fqdn\":\"$fulldomain\"}"
|
||||
res=$(_api_call "$Beget_Api/dns/getData" "$data")
|
||||
if ! _is_api_reply_ok "$res"; then
|
||||
_err "Can't get domain records."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Add new TXT record"
|
||||
data="{\"fqdn\":\"$fulldomain\",\"records\":{"
|
||||
data=${data}$(_parce_records "$res" "A")
|
||||
data=${data}$(_parce_records "$res" "AAAA")
|
||||
data=${data}$(_parce_records "$res" "CAA")
|
||||
data=${data}$(_parce_records "$res" "MX")
|
||||
data=${data}$(_parce_records "$res" "SRV")
|
||||
data=${data}$(_parce_records "$res" "TXT")
|
||||
data=$(echo "$data" | sed 's/,$//')
|
||||
data=${data}'}}'
|
||||
|
||||
str=$(_txt_to_dns_json "$txtvalue")
|
||||
data=$(_add_record "$data" "TXT" "$str")
|
||||
|
||||
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
|
||||
if ! _is_api_reply_ok "$res"; then
|
||||
_err "Can't change domain records."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: fulldomain txtvalue
|
||||
# Used to remove the txt record after validation
|
||||
dns_beget_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
_debug "dns_beget_rm() $fulldomain $txtvalue"
|
||||
fulldomain=$(echo "$fulldomain" | _lower_case)
|
||||
|
||||
Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}"
|
||||
Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}"
|
||||
|
||||
_info "Get current domain records"
|
||||
data="{\"fqdn\":\"$fulldomain\"}"
|
||||
res=$(_api_call "$Beget_Api/dns/getData" "$data")
|
||||
if ! _is_api_reply_ok "$res"; then
|
||||
_err "Can't get domain records."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Remove TXT record"
|
||||
data="{\"fqdn\":\"$fulldomain\",\"records\":{"
|
||||
data=${data}$(_parce_records "$res" "A")
|
||||
data=${data}$(_parce_records "$res" "AAAA")
|
||||
data=${data}$(_parce_records "$res" "CAA")
|
||||
data=${data}$(_parce_records "$res" "MX")
|
||||
data=${data}$(_parce_records "$res" "SRV")
|
||||
data=${data}$(_parce_records "$res" "TXT")
|
||||
data=$(echo "$data" | sed 's/,$//')
|
||||
data=${data}'}}'
|
||||
|
||||
str=$(_txt_to_dns_json "$txtvalue")
|
||||
data=$(_rm_record "$data" "$str")
|
||||
|
||||
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
|
||||
if ! _is_api_reply_ok "$res"; then
|
||||
_err "Can't change domain records."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ####################
|
||||
|
||||
# Create subdomain if needed
|
||||
# Usage: _prepare_subdomain [fulldomain]
|
||||
_prepare_subdomain() {
|
||||
fulldomain=$1
|
||||
|
||||
_info "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"
|
||||
|
||||
if [ -z "$_sub_domain" ]; then
|
||||
_debug "$fulldomain is a root domain."
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "Get subdomain list"
|
||||
res=$(_api_call "$Beget_Api/domain/getSubdomainList")
|
||||
if ! _is_api_reply_ok "$res"; then
|
||||
_err "Can't get subdomain list."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$res" "\"fqdn\":\"$fulldomain\""; then
|
||||
_debug "Subdomain $fulldomain already exist."
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "Subdomain $fulldomain does not exist. Let's create one."
|
||||
data="{\"subdomain\":\"$_sub_domain\",\"domain_id\":$_domain_id}"
|
||||
res=$(_api_call "$Beget_Api/domain/addSubdomainVirtual" "$data")
|
||||
if ! _is_api_reply_ok "$res"; then
|
||||
_err "Can't create subdomain."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Cleanup subdomen records"
|
||||
data="{\"fqdn\":\"$fulldomain\",\"records\":{}}"
|
||||
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
|
||||
if ! _is_api_reply_ok "$res"; then
|
||||
_debug "Can't cleanup $fulldomain records."
|
||||
fi
|
||||
|
||||
data="{\"fqdn\":\"www.$fulldomain\",\"records\":{}}"
|
||||
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
|
||||
if ! _is_api_reply_ok "$res"; then
|
||||
_debug "Can't cleanup www.$fulldomain records."
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: _get_root _acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
# _domain_id=32436365
|
||||
_get_root() {
|
||||
fulldomain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
_debug "Get domain list"
|
||||
res=$(_api_call "$Beget_Api/domain/getList")
|
||||
if ! _is_api_reply_ok "$res"; then
|
||||
_err "Can't get domain list."
|
||||
return 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
|
||||
if [ -z "$h" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$res" "$h"; then
|
||||
_domain_id=$(echo "$res" | _egrep_o "\"id\":[0-9]*,\"fqdn\":\"$h\"" | cut -d , -f1 | cut -d : -f2)
|
||||
if [ "$_domain_id" ]; then
|
||||
if [ "$h" != "$fulldomain" ]; then
|
||||
_sub_domain=$(echo "$fulldomain" | cut -d . -f 1-"$p")
|
||||
else
|
||||
_sub_domain=""
|
||||
fi
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
p="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Parce DNS records from json string
|
||||
# Usage: _parce_records [j_str] [record_name]
|
||||
_parce_records() {
|
||||
j_str=$1
|
||||
record_name=$2
|
||||
res="\"$record_name\":["
|
||||
res=${res}$(echo "$j_str" | _egrep_o "\"$record_name\":\[.*" | cut -d '[' -f2 | cut -d ']' -f1)
|
||||
res=${res}"],"
|
||||
echo "$res"
|
||||
}
|
||||
|
||||
# Usage: _add_record [data] [record_name] [record_data]
|
||||
_add_record() {
|
||||
data=$1
|
||||
record_name=$2
|
||||
record_data=$3
|
||||
echo "$data" | sed "s/\"$record_name\":\[/\"$record_name\":\[$record_data,/" | sed "s/,\]/\]/"
|
||||
}
|
||||
|
||||
# Usage: _rm_record [data] [record_data]
|
||||
_rm_record() {
|
||||
data=$1
|
||||
record_data=$2
|
||||
echo "$data" | sed "s/$record_data//g" | sed "s/,\+/,/g" |
|
||||
sed "s/{,/{/g" | sed "s/,}/}/g" |
|
||||
sed "s/\[,/\[/g" | sed "s/,\]/\]/g"
|
||||
}
|
||||
|
||||
_txt_to_dns_json() {
|
||||
echo "{\"ttl\":600,\"txtdata\":\"$1\"}"
|
||||
}
|
||||
|
||||
# Usage: _api_call [api_url] [input_data]
|
||||
_api_call() {
|
||||
api_url="$1"
|
||||
input_data="$2"
|
||||
|
||||
_debug "_api_call $api_url"
|
||||
_debug "Request: $input_data"
|
||||
|
||||
# res=$(curl -s -L -D ./http.header \
|
||||
# "$api_url" \
|
||||
# --data-urlencode login=$Beget_Username \
|
||||
# --data-urlencode passwd=$Beget_Password \
|
||||
# --data-urlencode input_format=json \
|
||||
# --data-urlencode output_format=json \
|
||||
# --data-urlencode "input_data=$input_data")
|
||||
|
||||
url="$api_url?login=$Beget_Username&passwd=$Beget_Password&input_format=json&output_format=json"
|
||||
if [ -n "$input_data" ]; then
|
||||
url=${url}"&input_data="
|
||||
url=${url}$(echo "$input_data" | _url_encode)
|
||||
fi
|
||||
res=$(_get "$url")
|
||||
|
||||
_debug "Reply: $res"
|
||||
echo "$res"
|
||||
}
|
||||
|
||||
# Usage: _is_api_reply_ok [api_reply]
|
||||
_is_api_reply_ok() {
|
||||
_contains "$1" '^{"status":"success","answer":{"status":"success","result":.*}}$'
|
||||
}
|
||||
202
dnsapi/dns_bh.sh
202
dnsapi/dns_bh.sh
|
|
@ -1,202 +0,0 @@
|
|||
#!/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
|
||||
}
|
||||
|
|
@ -1,373 +0,0 @@
|
|||
#!/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"[ ]*:[ ]*[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 '(^|[^0-9a-zA-Z])id[ ]*=[ ]*[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[ ]*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"
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ Options:
|
|||
BOOKMYNAME_USERNAME Username
|
||||
BOOKMYNAME_PASSWORD Password
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/3209
|
||||
Author: @Neilpang
|
||||
Author: Neilpang
|
||||
'
|
||||
|
||||
######## Public functions #####################
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ _get_base_domain() {
|
|||
_debug2 domain_list "$domain_list"
|
||||
|
||||
i=1
|
||||
while [ "$i" -gt 0 ]; do
|
||||
while [ $i -gt 0 ]; do
|
||||
## get next longest domain
|
||||
_domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM")
|
||||
## check we got something back from our cut (or are we at the end)
|
||||
|
|
@ -208,7 +208,7 @@ _get_base_domain() {
|
|||
## check if it exists
|
||||
if [ -n "$found" ]; then
|
||||
## exists - exit loop returning the parts
|
||||
sub_point=$(_math "$i" - 1)
|
||||
sub_point=$(_math $i - 1)
|
||||
_sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point")
|
||||
_domain_id="$(echo "$found" | _egrep_o "Id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")"
|
||||
_debug _domain_id "$_domain_id"
|
||||
|
|
@ -218,11 +218,11 @@ _get_base_domain() {
|
|||
return 0
|
||||
fi
|
||||
## increment cut point $i
|
||||
i=$(_math "$i" + 1)
|
||||
i=$(_math $i + 1)
|
||||
done
|
||||
|
||||
if [ -z "$found" ]; then
|
||||
page=$(_math "$page" + 1)
|
||||
page=$(_math $page + 1)
|
||||
nextpage="https://api.bunny.net/dnszone?page=$page"
|
||||
## Find the next page if we don't have a match.
|
||||
hasnextpage="$(echo "$domain_list" | _egrep_o "\"HasMoreItems\"\s*:\s*true")"
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
#!/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
|
||||
}
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
#!/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
|
||||
}
|
||||
|
|
@ -92,9 +92,7 @@ dns_cf_add() {
|
|||
if _contains "$response" "$txtvalue"; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
elif _contains "$response" "The record already exists" ||
|
||||
_contains "$response" "An identical record already exists." ||
|
||||
_contains "$response" '"code":81058'; then
|
||||
elif _contains "$response" "The record already exists"; then
|
||||
_info "Already exists, OK"
|
||||
return 0
|
||||
else
|
||||
|
|
@ -188,7 +186,7 @@ _get_root() {
|
|||
fi
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
h=$(printf "%s" "$domain" | cut -d . -f $i-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
|
|
@ -208,7 +206,7 @@ _get_root() {
|
|||
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_count":1'; then
|
||||
_domain_id=$(echo "$response" | _egrep_o "\[.\"id\": *\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d " ")
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -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. Server response: $response"
|
||||
_err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Please check your login credentials."
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ _dns_cloudns_get_zone_info() {
|
|||
_dns_cloudns_get_zone_name() {
|
||||
i=2
|
||||
while true; do
|
||||
zoneForCheck=$(printf "%s" "$1" | cut -d . -f "$i"-100)
|
||||
zoneForCheck=$(printf "%s" "$1" | cut -d . -f $i-100)
|
||||
|
||||
if [ -z "$zoneForCheck" ]; then
|
||||
return 1
|
||||
|
|
@ -197,11 +197,10 @@ _dns_cloudns_http_api_call() {
|
|||
auth_user="auth-id=$CLOUDNS_AUTH_ID"
|
||||
fi
|
||||
|
||||
encoded_password=$(echo "$CLOUDNS_AUTH_PASSWORD" | tr -d "\n\r" | _url_encode)
|
||||
if [ -z "$2" ]; then
|
||||
data="$auth_user&auth-password=$encoded_password"
|
||||
data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD"
|
||||
else
|
||||
data="$auth_user&auth-password=$encoded_password&$2"
|
||||
data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD&$2"
|
||||
fi
|
||||
|
||||
response="$(_get "$CLOUDNS_API/$method?$data")"
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue