diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 0a121fd7..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -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. diff --git a/.github/workflows/Apache.yml b/.github/workflows/Apache.yml deleted file mode 100644 index b17abbd1..00000000 --- a/.github/workflows/Apache.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 84a17470..be9d3aae 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -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 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" - diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index 20c61dcc..5c56168f 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -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" + diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index 88ef0a6e..961907e8 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -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" + diff --git a/.github/workflows/GhostBSD.yml b/.github/workflows/GhostBSD.yml deleted file mode 100644 index 04510c15..00000000 --- a/.github/workflows/GhostBSD.yml +++ /dev/null @@ -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" diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml deleted file mode 100644 index b133dd18..00000000 --- a/.github/workflows/Haiku.yml +++ /dev/null @@ -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" - diff --git a/.github/workflows/Hurd.yml b/.github/workflows/Hurd.yml deleted file mode 100644 index fee80d29..00000000 --- a/.github/workflows/Hurd.yml +++ /dev/null @@ -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" diff --git a/.github/workflows/Linux.yml b/.github/workflows/Linux.yml index 17462033..c74e9d3e 100644 --- a/.github/workflows/Linux.yml +++ b/.github/workflows/Linux.yml @@ -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 .. \ diff --git a/.github/workflows/MacOS.yml b/.github/workflows/MacOS.yml index ef9580a6..f5d73ec9 100644 --- a/.github/workflows/MacOS.yml +++ b/.github/workflows/MacOS.yml @@ -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 .. \ diff --git a/.github/workflows/MidnightBSD.yml b/.github/workflows/MidnightBSD.yml deleted file mode 100644 index ce499e4e..00000000 --- a/.github/workflows/MidnightBSD.yml +++ /dev/null @@ -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" diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index 6695f71e..a4f90f68 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -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" - + + diff --git a/.github/workflows/Nginx.yml b/.github/workflows/Nginx.yml deleted file mode 100644 index 2ca9d64a..00000000 --- a/.github/workflows/Nginx.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml index aabb168b..882cedf6 100644 --- a/.github/workflows/Omnios.yml +++ b/.github/workflows/Omnios.yml @@ -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" + diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index 46318163..d5697c10 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -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" + diff --git a/.github/workflows/OpenEuler.yml b/.github/workflows/OpenEuler.yml deleted file mode 100644 index 2b4bd0ab..00000000 --- a/.github/workflows/OpenEuler.yml +++ /dev/null @@ -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" diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml deleted file mode 100644 index e3119f8e..00000000 --- a/.github/workflows/OpenIndiana.yml +++ /dev/null @@ -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" - diff --git a/.github/workflows/PebbleStrict.yml b/.github/workflows/PebbleStrict.yml index 946d993a..b0326332 100644 --- a/.github/workflows/PebbleStrict.yml +++ b/.github/workflows/PebbleStrict.yml @@ -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 diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 30e4e291..95bcd8d1 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -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" + diff --git a/.github/workflows/Tribblix.yml b/.github/workflows/Tribblix.yml deleted file mode 100644 index 68e61dc8..00000000 --- a/.github/workflows/Tribblix.yml +++ /dev/null @@ -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" diff --git a/.github/workflows/Ubuntu.yml b/.github/workflows/Ubuntu.yml index 36dfdbe6..e580828f 100644 --- a/.github/workflows/Ubuntu.yml +++ b/.github/workflows/Ubuntu.yml @@ -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 diff --git a/.github/workflows/Windows.yml b/.github/workflows/Windows.yml index 1de120f9..c1fd1085 100644 --- a/.github/workflows/Windows.yml +++ b/.github/workflows/Windows.yml @@ -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/ diff --git a/.github/workflows/blacklist-command.yml b/.github/workflows/blacklist-command.yml deleted file mode 100644 index 35e53677..00000000 --- a/.github/workflows/blacklist-command.yml +++ /dev/null @@ -1,114 +0,0 @@ -name: Blacklist Command - -# An issue titled "blacklist: " 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 .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" - }); diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 7dc42290..49173b4b 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -41,29 +41,23 @@ 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 + uses: actions/checkout@v4 with: persist-credentials: false - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@v2 - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@v5.5.1 with: images: ${DOCKER_IMAGE} - 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: | if [[ $GITHUB_REF == refs/tags/* ]]; then @@ -79,8 +73,6 @@ jobs: fi fi - echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV" - DOCKER_LABELS=() while read -r label; do DOCKER_LABELS+=(--label "${label}") @@ -92,9 +84,3 @@ jobs: --output "type=image,push=true" \ --build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \ --platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x . - - name: mirror the image to ghcr (best-effort) - run: | - docker buildx imagetools create \ - --tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \ - ${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \ - || echo "::warning::GHCR mirror failed; Docker Hub publish unaffected" diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index a25cd4ef..e92b0411 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -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." + }) \ No newline at end of file diff --git a/.github/workflows/pr_dns.yml b/.github/workflows/pr_dns.yml index 19763a15..58630e8b 100644 --- a/.github/workflows/pr_dns.yml +++ b/.github/workflows/pr_dns.yml @@ -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,12 @@ 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. + Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2 + We look forward to reviewing your Pull request shortly ✨ + 注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试. ` }) diff --git a/.github/workflows/pr_notify.yml b/.github/workflows/pr_notify.yml index 76ae76f6..b6b03c67 100644 --- a/.github/workflows/pr_notify.yml +++ b/.github/workflows/pr_notify.yml @@ -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({ diff --git a/.github/workflows/revert-command.yml b/.github/workflows/revert-command.yml deleted file mode 100644 index 03161bbb..00000000 --- a/.github/workflows/revert-command.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Revert Command - -# An issue titled "revert: " 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 .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" - }); diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index eb10b2b0..746727d4 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -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 diff --git a/.github/workflows/vtag.yml b/.github/workflows/vtag.yml deleted file mode 100644 index e9f7e8df..00000000 --- a/.github/workflows/vtag.yml +++ /dev/null @@ -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 }}" diff --git a/.github/workflows/wiki-guard.yml b/.github/workflows/wiki-guard.yml deleted file mode 100644 index dbe3dd8d..00000000 --- a/.github/workflows/wiki-guard.yml +++ /dev/null @@ -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 .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 diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml deleted file mode 100644 index 89bb1f3a..00000000 --- a/.github/workflows/wiki-monitor.yml +++ /dev/null @@ -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 - - - - - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 33294ce7..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -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. diff --git a/Dockerfile b/Dockerfile index 229e4830..7523f0af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.23 +FROM alpine:3.21 RUN apk --no-cache add -f \ openssl \ @@ -13,15 +13,10 @@ RUN apk --no-cache add -f \ tar \ libidn \ jq \ - yq-go \ - supercronic - -ENV LE_WORKING_DIR=/acmebin + cronie ENV LE_CONFIG_HOME=/acme.sh -ENV HOME=/acme.sh - ARG AUTO_UPGRADE=1 ENV AUTO_UPGRADE=$AUTO_UPGRADE @@ -32,13 +27,10 @@ COPY ./deploy /install_acme.sh/deploy COPY ./dnsapi /install_acme.sh/dnsapi COPY ./notify /install_acme.sh/notify -RUN addgroup -g 1000 acme && adduser -h $LE_CONFIG_HOME -s /bin/sh -G acme -D -H -u 1000 acme - RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/ -RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh -RUN 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 +64,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 diff --git a/README.md b/README.md index 90280e94..9a5c106b 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,52 @@ -

- - - - - - - - ZeroSSL - - -

+# An ACME Shell script: acme.sh -

🔐 acme.sh

-

An ACME Protocol Client Written Purely in Shell

+[![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml) +[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml) +[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml) +[![MacOS](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml) +[![Ubuntu](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml) +[![Windows](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml) +[![Solaris](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml) +[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml) +[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml) -

- FreeBSD - OpenBSD - NetBSD - MacOS - Ubuntu - Windows - Solaris - DragonFlyBSD - MidnightBSD - GhostBSD - Omnios - OpenIndiana - Tribblix - Haiku - Hurd - OpenEuler -

- -

- Shellcheck - PebbleStrict - DockerHub -

- -

- Financial Contributors on Open Collective - Join the chat at Gitter - Docker stars - Docker pulls -

+![Shellcheck](https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg) +![PebbleStrict](https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg) +![DockerHub](https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg) ---- + +[![Join the chat at https://gitter.im/acme-sh/Lobby](https://badges.gitter.im/acme-sh/Lobby.svg)](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Docker stars](https://img.shields.io/docker/stars/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub") +[![Docker pulls](https://img.shields.io/docker/pulls/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub") -## ✨ Features -- 🐚 An ACME protocol client written **purely in Shell** (Unix shell) language -- 📜 Full ACME protocol implementation -- 🔑 Support **ECDSA** certificates -- 🌐 Support **SAN** and **wildcard** certificates -- ⚡ Simple, powerful and very easy to use — only **3 minutes** to learn! -- 🔧 Compatible with **Bash**, **dash** and **sh** -- 🚫 No dependencies on Python -- 🔄 One script to issue, renew and install your certificates automatically -- 👤 **DOES NOT** require `root/sudoer` access -- 🐳 Docker ready -- 🌍 IPv6 ready -- 📧 Cron job notifications for renewal or error -> 💡 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. -

- 📚 Wiki • - 🐳 Docker Guide • - 🐦 Twitter -

+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|[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)|OpenBSD |8|[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)|NetBSD |9|[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)|DragonFlyBSD -|10|[![MidnightBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml)|MidnightBSD -|11|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios -|12|[![OpenIndiana](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml)|OpenIndiana -|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian -|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE -|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl) -|16|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux -|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora -|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux -|19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux -|20|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia -|21|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux +|10|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios +|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian +|12|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|CentOS +|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE +|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl) +|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux +|16|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora +|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux +|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux +|19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia +|10|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux +|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|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://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) -|25|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS -|26|[![Tribblix](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix -|27|[![GhostBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD -|28|[![Hurd](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd -|29|[![OpenEuler](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg)](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 ` | 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 ` | 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 ` | Adds `persistUntil=` 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=`** 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. - -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 -#### 🏢 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 ---- -### 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 ZeroSSL 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. -

- - - - - ZeroSSL - - -

+[Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list) diff --git a/acme.sh b/acme.sh index 4d4f6cbe..dd21785d 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.1.5 +VER=3.1.1 PROJECT_NAME="acme.sh" @@ -23,6 +23,9 @@ _SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY" CA_LETSENCRYPT_V2="https://acme-v02.api.letsencrypt.org/directory" CA_LETSENCRYPT_V2_TEST="https://acme-staging-v02.api.letsencrypt.org/directory" +CA_BUYPASS="https://api.buypass.com/acme/directory" +CA_BUYPASS_TEST="https://api.test4.buypass.no/acme/directory" + CA_ZEROSSL="https://acme.zerossl.com/v2/DV90" _ZERO_EAB_ENDPOINT="https://api.zerossl.com/acme/eab-credentials-email" @@ -32,8 +35,6 @@ CA_SSLCOM_ECC="https://acme.ssl.com/sslcom-dv-ecc" CA_GOOGLE="https://dv.acme-v02.api.pki.goog/directory" CA_GOOGLE_TEST="https://dv.acme-v02.test-api.pki.goog/directory" -CA_ACTALIS="https://acme-api.actalis.com/acme/directory" - DEFAULT_CA=$CA_ZEROSSL DEFAULT_STAGING_CA=$CA_LETSENCRYPT_V2_TEST @@ -41,13 +42,14 @@ CA_NAMES=" ZeroSSL.com,zerossl LetsEncrypt.org,letsencrypt LetsEncrypt.org_test,letsencrypt_test,letsencrypttest +BuyPass.com,buypass +BuyPass.com_test,buypass_test,buypasstest SSL.com,sslcom Google.com,google Google.com_test,googletest,google_test -Actalis.com,actalis.com,actalis " -CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST,$CA_ACTALIS" +CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST" DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)" @@ -59,20 +61,18 @@ DEFAULT_OPENSSL_BIN="openssl" VTYPE_HTTP="http-01" VTYPE_DNS="dns-01" VTYPE_ALPN="tls-alpn-01" -VTYPE_DNS_PERSIST="dns-persist-01" ID_TYPE_DNS="dns" ID_TYPE_IP="ip" LOCAL_ANY_ADDRESS="0.0.0.0" -DEFAULT_RENEW="${DEFAULT_RENEW:-30}" +DEFAULT_RENEW=60 NO_VALUE="no" W_DNS="dns" W_ALPN="alpn" -W_DNS_PERSIST="dns_persist" DNS_ALIAS_PREFIX="=" MODE_STATELESS="stateless" @@ -180,8 +180,6 @@ _VALIDITY_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Validity" _DNSCHECK_WIKI="https://github.com/acmesh-official/acme.sh/wiki/dnscheck" -_PROFILESELECTION_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Profile-selection" - _DNS_MANUAL_ERR="The dns manual mode can not renew automatically, you must issue it again manually. You'd better use the other modes instead." _DNS_MANUAL_WARN="It seems that you are using dns manual mode. please take care: $_DNS_MANUAL_ERR" @@ -252,13 +250,6 @@ _dlg_versions() { socat -V 2>&1 else _debug "socat doesn't exist." - if _exists "python3"; then - python3 -V 2>&1 - elif _exists "python2"; then - python2 -V 2>&1 - elif _exists "python"; then - python -V 2>&1 - fi fi } @@ -445,28 +436,14 @@ _secure_debug3() { fi } -__USE_TR_TAG="" -if [ "$(echo "abc" | LANG=C tr a-z A-Z 2>/dev/null)" != "ABC" ]; then - __USE_TR_TAG="1" -fi -export __USE_TR_TAG - _upper_case() { - if [ "$__USE_TR_TAG" ]; then - LANG=C tr '[:lower:]' '[:upper:]' - else - # shellcheck disable=SC2018,SC2019 - LANG=C tr '[a-z]' '[A-Z]' - fi + # shellcheck disable=SC2018,SC2019 + tr '[a-z]' '[A-Z]' } _lower_case() { - if [ "$__USE_TR_TAG" ]; then - LANG=C tr '[:upper:]' '[:lower:]' - else - # shellcheck disable=SC2018,SC2019 - LANG=C tr '[A-Z]' '[a-z]' - fi + # shellcheck disable=SC2018,SC2019 + tr '[A-Z]' '[a-z]' } _startswith() { @@ -597,6 +574,11 @@ if [ "$(printf '\x41')" != 'A' ]; then _URGLY_PRINTF=1 fi +_ESCAPE_XARGS="" +if _exists xargs && [ "$(printf %s '\\x41' | xargs printf)" = 'A' ]; then + _ESCAPE_XARGS=1 +fi + _h2b() { if _exists xxd; then if _contains "$(xxd --help 2>&1)" "assumes -c30"; then @@ -615,8 +597,17 @@ _h2b() { jc="" _debug2 _URGLY_PRINTF "$_URGLY_PRINTF" if [ -z "$_URGLY_PRINTF" ]; then - # shellcheck disable=SC2059 - printf "$(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/\\x\1/g')" + if [ "$_ESCAPE_XARGS" ] && _exists xargs; then + _debug2 "xargs" + echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/g' | xargs printf + else + for h in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/ \1/g'); do + if [ -z "$h" ]; then + break + fi + printf "\x$h%s" + done + fi else for c in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\)/ \1/g'); do if [ -z "$ic" ]; then @@ -918,15 +909,6 @@ _json_decode() { echo "$_j_str" } -#extract the authorization URLs from an order response on stdin, as a -#comma-separated list. The entries are quoted URL strings and a quote cannot -#occur inside a URL, so the first '"]' is always the end of the array. A -#char-class scan would stop early on the brackets of an IPv6 host -#(https://[2001:db8::1]/...). Outputs nothing if the field is missing. -_authorizations_from_order() { - sed -n 's/.*"authorizations" *: *\[//p' | sed 's/" *\].*//' | tr -d '" ' -} - #options file _sed_i() { options="$1" @@ -1024,24 +1006,6 @@ _checkcert() { fi } -#file -_enddate() { - _cf="$1" - _res="$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -enddate -in "$_cf")" - if [ "$?" != "0" ] || [ -z "$_res" ]; then - return 1 - fi - - case "$_res" in - notAfter=*) - echo "${_res#notAfter=}" - ;; - *) - return 1 - ;; - esac -} - #Usage: hashalg [outputhex] #Output Base64-encoded digest _digest() { @@ -1053,7 +1017,7 @@ _digest() { outputhex="$2" - if [ "$alg" = "sha3-256" ] || [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then + if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then if [ "$outputhex" ]; then ${ACME_OPENSSL_BIN:-openssl} dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' ' else @@ -1066,25 +1030,6 @@ _digest() { } -#Usage: certpath hashalg -#Output certificate fingerprint without colons -_fingerprint() { - cert="$1" - alg="$2" - if [ -z "$alg" ]; then - _usage "Usage: _fingerprint certpath hashalg" - return 1 - fi - - if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then - # openssl prints "SHA1 Fingerprint=AA:BB:CC:..."; strip prefix and colons. - ${ACME_OPENSSL_BIN:-openssl} x509 -in "$cert" -noout -fingerprint -"$alg" | sed 's/.*=//; s/://g' - else - _err "$alg is not supported yet" - return 1 - fi -} - #Usage: hashalg secret_hex [outputhex] #Output binary hmac _hmac() { @@ -1188,11 +1133,6 @@ _createkey() { length="$1" f="$2" _debug2 "_createkey for file:$f" - if ! _exists "${ACME_OPENSSL_BIN:-openssl}"; then - _err "Please install openssl first. ACME_OPENSSL_BIN=$ACME_OPENSSL_BIN" - _err "We need openssl to generate keys." - return 1 - fi eccname="$length" if _startswith "$length" "ec-"; then length=$(printf "%s" "$length" | cut -d '-' -f 2-100) @@ -1215,7 +1155,6 @@ _createkey() { _debug "Using length $length" - _new_key_file="" if ! [ -e "$f" ]; then if ! touch "$f" >/dev/null 2>&1; then _f_path="$(dirname "$f")" @@ -1229,7 +1168,6 @@ _createkey() { return 1 fi chmod 600 "$f" - _new_key_file="1" fi if _isEccKey "$length"; then @@ -1238,10 +1176,6 @@ _createkey() { echo "$_opkey" >"$f" else _err "Error encountered for ECC key named $eccname" - #do not leave an empty file behind, or the next run would treat the key as existing - if [ "$_new_key_file" ]; then - rm -f "$f" - fi return 1 fi else @@ -1254,10 +1188,6 @@ _createkey() { echo "$_opkey" >"$f" else _err "Error encountered for RSA key of length $length" - #do not leave an empty file behind, or the next run would treat the key as existing - if [ "$_new_key_file" ]; then - rm -f "$f" - fi return 1 fi fi @@ -1306,24 +1236,7 @@ _idn() { fi } -#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 extendedUsage -#cn -#The x509 Common Name is limited to 64 characters (RFC 5280 ub-common-name, -#enforced by openssl in ASN1_mbstring_ncopy), and an IP address or an empty -#name is not usable as CN either. When this rejects the name, _createcsr -#omits CN from the CSR subject and the CA takes the identifiers from the -#subjectAltName extension (issue 4867). -_is_valid_cn() { - _cn_v="$1" - if [ -z "$_cn_v" ] || [ "${#_cn_v}" -gt 64 ]; then - return 1 - fi - if _isIP "$_cn_v"; then - return 1 - fi - return 0 -} - +#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 _createcsr() { _debug _createcsr domain="$1" @@ -1332,7 +1245,6 @@ _createcsr() { csr="$4" csrconf="$5" acmeValidationv1="$6" - extusage="$7" _debug2 domain "$domain" _debug2 domainlist "$domainlist" _debug2 csrkey "$csrkey" @@ -1341,8 +1253,9 @@ _createcsr() { printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]" >"$csrconf" - if [ "$extusage" ]; then - printf "\nextendedKeyUsage=$extusage\n" >>"$csrconf" + if [ "$Le_ExtKeyUse" ]; then + _savedomainconf Le_ExtKeyUse "$Le_ExtKeyUse" + printf "\nextendedKeyUsage=$Le_ExtKeyUse\n" >>"$csrconf" else printf "\nextendedKeyUsage=serverAuth,clientAuth\n" >>"$csrconf" fi @@ -1387,16 +1300,16 @@ _createcsr() { _csr_cn="$(_idn "$domain")" _debug2 _csr_cn "$_csr_cn" if _contains "$(uname -a)" "MINGW"; then - if _is_valid_cn "$_csr_cn"; then - ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//CN=$_csr_cn" -config "$csrconf" -out "$csr" - else + if _isIP "$_csr_cn"; then ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//O=$PROJECT_NAME" -config "$csrconf" -out "$csr" + else + ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//CN=$_csr_cn" -config "$csrconf" -out "$csr" fi else - if _is_valid_cn "$_csr_cn"; then - ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr" - else + if _isIP "$_csr_cn"; then ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/O=$PROJECT_NAME" -config "$csrconf" -out "$csr" + else + ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr" fi fi } @@ -1422,9 +1335,7 @@ _readSubjectFromCSR() { _usage "_readSubjectFromCSR mycsr.csr" return 1 fi - # -config /dev/null: reading a CSR needs no config, but a missing default - # openssl.cnf is fatal on some systems (e.g. NetBSD does not install one) - ${ACME_OPENSSL_BIN:-openssl} req -noout -in "$_csrfile" -subject -config /dev/null | tr ',' "\n" | _egrep_o "CN *=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d ' \n' + ${ACME_OPENSSL_BIN:-openssl} req -noout -in "$_csrfile" -subject | tr ',' "\n" | _egrep_o "CN *=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d ' \n' } #_csrfile @@ -1439,17 +1350,16 @@ _readSubjectAltNamesFromCSR() { _csrsubj="$(_readSubjectFromCSR "$_csrfile")" _debug _csrsubj "$_csrsubj" - _dnsAltnames="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile" -config /dev/null | grep "^ *DNS:.*" | tr -d ' \n')" + _dnsAltnames="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile" | grep "^ *DNS:.*" | tr -d ' \n')" _debug _dnsAltnames "$_dnsAltnames" - # escape the wildcard '*' so it is not taken as a regex operator by grep/sed below - _escapedAltnames="$(echo "$_dnsAltnames" | tr '*' '#')" - _debug _escapedAltnames "$_escapedAltnames" - _escapedSubject="$(echo "$_csrsubj" | tr '*' '#')" - _debug _escapedSubject "$_escapedSubject" - if _contains "$_escapedAltnames," "DNS:$_escapedSubject,"; then + if _contains "$_dnsAltnames," "DNS:$_csrsubj,"; then _debug "AltNames contains subject" - _dnsAltnames="$(echo "$_escapedAltnames," | sed "s/DNS:$_escapedSubject,//g" | tr '#' '*' | sed "s/,\$//g")" + _excapedAlgnames="$(echo "$_dnsAltnames" | tr '*' '#')" + _debug _excapedAlgnames "$_excapedAlgnames" + _escapedSubject="$(echo "$_csrsubj" | tr '*' '#')" + _debug _escapedSubject "$_escapedSubject" + _dnsAltnames="$(echo "$_excapedAlgnames," | sed "s/DNS:$_escapedSubject,//g" | tr '#' '*' | sed "s/,\$//g")" _debug _dnsAltnames "$_dnsAltnames" else _debug "AltNames doesn't contain subject" @@ -1466,7 +1376,7 @@ _readKeyLengthFromCSR() { return 1 fi - _outcsr="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile" -config /dev/null)" + _outcsr="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile")" _debug2 _outcsr "$_outcsr" if _contains "$_outcsr" "Public Key Algorithm: id-ecPublicKey"; then _debug "ECC CSR" @@ -1491,12 +1401,6 @@ _ss() { return 0 fi - if [ "$(uname)" = "AIX" ]; then - _debug "Using: AIX netstat" - netstat -an | grep "^tcp" | grep "LISTEN" | grep "\.$_port " - return 0 - fi - if _exists "netstat"; then _debug "Using: netstat" if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then @@ -1542,23 +1446,7 @@ _toPkcs() { ${ACME_OPENSSL_BIN:-openssl} pkcs12 -export -out "$_cpfx" -inkey "$_ckey" -in "$_ccert" -certfile "$_cca" fi if [ "$?" = "0" ]; then - _savedomainconf "Le_PFXPassword" "$pfxPassword" "base64" - fi - -} - -_toPkcs8() { - _cpkcs8="$1" - _ckey="$2" - pkcs8Password="$3" - - if [ "$pkcs8Password" ]; then - ${ACME_OPENSSL_BIN:-openssl} pkcs8 -topk8 -inform PEM -outform PEM -v2 aes256 -passout "pass:$pkcs8Password" -in "$_ckey" -out "$_cpkcs8" - else - ${ACME_OPENSSL_BIN:-openssl} pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in "$_ckey" -out "$_cpkcs8" - fi - if [ "$?" = "0" ]; then - _savedomainconf "Le_PKCS8Password" "$pkcs8Password" "base64" + _savedomainconf "Le_PFXPassword" "$pfxPassword" fi } @@ -1584,21 +1472,20 @@ toPkcs() { } -#domain [password] [isEcc] +#domain [isEcc] toPkcs8() { domain="$1" - pkcs8Password="$2" if [ -z "$domain" ]; then - _usage "Usage: $PROJECT_ENTRY --to-pkcs8 --domain [--password ] [--ecc]" + _usage "Usage: $PROJECT_ENTRY --to-pkcs8 --domain [--ecc]" return 1 fi - _isEcc="$3" + _isEcc="$2" _initpath "$domain" "$_isEcc" - _toPkcs8 "$CERT_PKCS8_PATH" "$CERT_KEY_PATH" "$pkcs8Password" + ${ACME_OPENSSL_BIN:-openssl} pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in "$CERT_KEY_PATH" -out "$CERT_PKCS8_PATH" if [ "$?" = "0" ]; then _info "Success, $CERT_PKCS8_PATH" @@ -1699,7 +1586,6 @@ createCSR() { domain="$1" domainlist="$2" _isEcc="$3" - _csreku="$4" _initpath "$domain" "$_isEcc" @@ -1713,7 +1599,7 @@ createCSR() { _err "Please create it first." return 1 fi - _createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_csreku" + _createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" } @@ -1919,10 +1805,6 @@ _time() { # 2022-04-01 08:10:33 to 1648800633 #or 2022-04-01T08:10:33Z to 1648800633 _date2time() { - #Mac/BSD - if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then - return - fi #Linux if date -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then return @@ -1932,89 +1814,26 @@ _date2time() { if gdate -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then return fi - #Omnios. Pass the date as argv (sys.argv[1]) instead of interpolating it into - #the -c program text, so a quote in the input cannot inject Python code. - if python3 -c "import datetime,sys; print(int(datetime.datetime.strptime(sys.argv[1], \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" "$1" 2>/dev/null; then + #Mac/BSD + if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then return fi #Omnios - if python3 -c "import datetime,sys; print(int(datetime.datetime.strptime(sys.argv[1], \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=datetime.timezone.utc).timestamp()))" "$1" 2>/dev/null; then + if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then + return + fi + #Omnios + if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then return fi _err "Cannot parse _date2time $1" return 1 } -#support the output format of openssl -enddate: -# Apr 01 08:10:33 2022 GMT to 1641283833 -_ssldate2time() { - #Linux - if date -u -d "$1" +"%s" 2>/dev/null; then - return - fi - #Solaris - if gdate -u -d "$1" +"%s" 2>/dev/null; then - return - fi - #Mac/BSD - if date -j -f "%b %d %T %Y %Z" "$1" +"%s" 2>/dev/null; then - return - fi - _err "Cannot parse _ssldate2time $1" - return 1 -} - _utc_date() { date -u "+%Y-%m-%d %H:%M:%S" } -#Usage: _calc_next_renew_time createtime renewaldays [endtime] -#Prints createtime + renewaldays*86400 - 86400, capped so it never passes -#the certificate expiry: with short-lived certs (internal CAs, upcoming -#CA/B SC-081 47-day maximum) a fixed RenewalDays would otherwise schedule -#the renewal after notAfter and leave an expired cert in place. -#The cap is one day before endtime, or one hour before for certs whose -#lifetime is 24 hours or less, mirroring the --valid-to scheduling. -_calc_next_renew_time() { - _cnrt_create="$1" - _cnrt_days="$2" - _cnrt_end="$3" - _cnrt_next=$(_math "$_cnrt_create" + "$_cnrt_days" \* 24 \* 60 \* 60 - 86400) - if [ -z "$_cnrt_end" ]; then - printf "%s" "$_cnrt_next" - return 0 - fi - if [ "$(_math "$_cnrt_end" - "$_cnrt_create")" -gt 86400 ]; then - _cnrt_cap=$(_math "$_cnrt_end" - 86400) - else - _cnrt_cap=$(_math "$_cnrt_end" - 3600) - fi - if [ "$_cnrt_next" -gt "$_cnrt_cap" ]; then - _cnrt_next="$_cnrt_cap" - fi - printf "%s" "$_cnrt_next" -} - -#Usage: _calc_validto_renew_time notaftertime renewaldays now -#Prints the next renew time for a cert issued with a relative --valid-to. -#A negative renewaldays is anchored to the expiry: notaftertime + -#renewaldays*86400. Otherwise the cert renews one day before the expiry, -#or one hour before for certs whose lifetime is 24 hours or less. -_calc_validto_renew_time() { - _cvrt_end="$1" - _cvrt_days="$2" - _cvrt_now="$3" - if [ "$_cvrt_days" ] && [ "$_cvrt_days" -lt 0 ]; then - _math "$_cvrt_end" + "$_cvrt_days" \* 24 \* 60 \* 60 - return 0 - fi - if [ "$(_math "$_cvrt_end" - "$_cvrt_now")" -gt 86400 ]; then - _math "$_cvrt_end" - 86400 - else - _math "$_cvrt_end" - 3600 - fi -} - _mktemp() { if _exists mktemp; then if mktemp 2>/dev/null; then @@ -2058,11 +1877,6 @@ _inithttp() { if [ -z "$_ACME_CURL" ] && _exists "curl"; then _ACME_CURL="curl --silent --dump-header $HTTP_HEADER " - if [ "$ACME_USE_IPV6_REQUESTS" ]; then - _ACME_CURL="$_ACME_CURL --ipv6 " - elif [ "$ACME_USE_IPV4_REQUESTS" ]; then - _ACME_CURL="$_ACME_CURL --ipv4 " - fi if [ -z "$ACME_HTTP_NO_REDIRECTS" ]; then _ACME_CURL="$_ACME_CURL -L " fi @@ -2090,11 +1904,6 @@ _inithttp() { if [ -z "$_ACME_WGET" ] && _exists "wget"; then _ACME_WGET="wget -q" - if [ "$ACME_USE_IPV6_REQUESTS" ]; then - _ACME_WGET="$_ACME_WGET --inet6-only " - elif [ "$ACME_USE_IPV4_REQUESTS" ]; then - _ACME_WGET="$_ACME_WGET --inet4-only " - fi if [ "$ACME_HTTP_NO_REDIRECTS" ]; then _ACME_WGET="$_ACME_WGET --max-redirect 0 " fi @@ -2512,7 +2321,6 @@ _setopt() { fi if [ ! -f "$__conf" ]; then touch "$__conf" - chmod 600 "$__conf" fi if [ -n "$(_tail_c 1 <"$__conf")" ]; then echo >>"$__conf" @@ -2641,13 +2449,6 @@ _savedeployconf() { _cleardomainconf "$1" } -#key -_cleardeployconf() { - _cleardomainconf "SAVED_$1" - #remove later - _cleardomainconf "$1" -} - #key _getdeployconf() { _rac_key="$1" @@ -2715,21 +2516,6 @@ _clearcaconf() { _clear_conf "$CA_CONF" "$1" } -#Starts a socat listener in the background, the pid is set to _socat_pid. -#It uses the content, _content_len, _NC and _SOCAT_ERR of _startserver. -#options -_startsocat() { - _socat_opts="$1" - _debug "_NC" "$_NC $_socat_opts" - $_NC $_socat_opts SYSTEM:"sleep 1; \ -echo 'HTTP/1.0 200 OK'; \ -echo 'Content-Length\: $_content_len'; \ -echo ''; \ -printf '%s' '$content';" 2>>"$_SOCAT_ERR" & - _socat_pid="$!" - _debug "_socat_pid" "$_socat_pid" -} - # content localaddress _startserver() { content="$1" @@ -2743,107 +2529,37 @@ _startserver() { _debug Le_Listen_V4 "$Le_Listen_V4" _debug Le_Listen_V6 "$Le_Listen_V6" - _serverproc_v6="" - if _exists "socat"; then - _NC="socat" - SOCAT_OPTIONS6="" - if [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then - _NC="$_NC -6" - SOCAT_OPTIONS=TCP6-LISTEN - elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then - _NC="$_NC -4" - SOCAT_OPTIONS=TCP4-LISTEN - elif [ "$ncaddr" ]; then - #a single local address belongs to a single family, let socat pick it - SOCAT_OPTIONS=TCP-LISTEN - else - #listen on both ipv4 and ipv6, with one socket for each family: - #ipv4-mapped ipv6 addresses are not available everywhere. - SOCAT_OPTIONS=TCP4-LISTEN - SOCAT_OPTIONS6=TCP6-LISTEN - fi - - if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then - _NC="$_NC -d -d -v" - fi - - SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork - if [ "$SOCAT_OPTIONS6" ]; then - #ipv6only keeps this socket from colliding with the ipv4 one - SOCAT_OPTIONS6=$SOCAT_OPTIONS6:$Le_HTTPPort,crlf,reuseaddr,fork,ipv6only=1 - fi - - #Adding bind to local-address - if [ "$ncaddr" ]; then - SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}" - fi - - _content_len="$(printf "%s" "$content" | wc -c)" - _debug _content_len "$_content_len" - export _SOCAT_ERR="$(_mktemp)" - _startsocat "$SOCAT_OPTIONS" - serverproc="$_socat_pid" - if [ "$SOCAT_OPTIONS6" ]; then - #best effort, the host may have no ipv6 support at all - _startsocat "$SOCAT_OPTIONS6" - _serverproc_v6="$_socat_pid" - fi + _NC="socat" + if [ "$Le_Listen_V6" ]; then + _NC="$_NC -6" else - _PYTHON="" - if _exists "python3"; then - _PYTHON="python3" - elif _exists "python2"; then - _PYTHON="python2" - elif _exists "python"; then - _PYTHON="python" - fi - if [ "$_PYTHON" ]; then - _debug "Using python: $_PYTHON" - #a comma separated list of addresses to listen on, one socket for each - _BIND_ADDR="0.0.0.0,::" - if [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then - _BIND_ADDR="::" - elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then - _BIND_ADDR="0.0.0.0" - fi - if [ "$ncaddr" ]; then - _BIND_ADDR="$ncaddr" - fi - _debug "_BIND_ADDR" "$_BIND_ADDR" - export _SOCAT_ERR="$(_mktemp)" - $_PYTHON -c "import socket,sys,select -res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3] -ads=sys.argv[2].split(',') -ls=[] -for ad in ads: - try: - sk=socket.socket(socket.AF_INET6 if ':' in ad else socket.AF_INET,socket.SOCK_STREAM) - sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) - if ':' in ad and len(ads)>1: - sk.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY,1) - sk.bind((ad,int(sys.argv[1]))) - sk.listen(5) - ls.append(sk) - except Exception: - sys.stderr.write(str(sys.exc_info()[1])+'\n') -if not ls: - sys.exit(1) -while True: - for sk in select.select(ls,[],[])[0]: - c,a=sk.accept() - c.sendall(res.encode() if hasattr(res, 'encode') else res) - c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" & - serverproc="$!" - _NC="$_PYTHON" - else - _err "Please install socat or python first for standalone mode." - return 1 - fi + _NC="$_NC -4" fi + if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then + _NC="$_NC -d -d -v" + fi + + SOCAT_OPTIONS=TCP-LISTEN:$Le_HTTPPort,crlf,reuseaddr,fork + + #Adding bind to local-address + if [ "$ncaddr" ]; then + SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}" + fi + + _content_len="$(printf "%s" "$content" | wc -c)" + _debug _content_len "$_content_len" + _debug "_NC" "$_NC $SOCAT_OPTIONS" + export _SOCAT_ERR="$(_mktemp)" + $_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \ +echo 'HTTP/1.0 200 OK'; \ +echo 'Content-Length\: $_content_len'; \ +echo ''; \ +printf '%s' '$content';" 2>"$_SOCAT_ERR" & + serverproc="$!" if [ -f "$_SOCAT_ERR" ]; then if grep "Permission denied" "$_SOCAT_ERR" >/dev/null; then - _err "$_NC: $(cat $_SOCAT_ERR)" + _err "socat: $(cat $_SOCAT_ERR)" _err "Can not listen for user: $(whoami)" _err "Maybe try with root again?" rm -f "$_SOCAT_ERR" @@ -2855,11 +2571,6 @@ while True: _stopserver() { pid="$1" _debug "pid" "$pid" - if [ "$_serverproc_v6" ]; then - _debug "_serverproc_v6" "$_serverproc_v6" - kill $_serverproc_v6 >/dev/null 2>&1 - _serverproc_v6="" - fi if [ -z "$pid" ]; then rm -f "$_SOCAT_ERR" return @@ -2933,11 +2644,9 @@ _starttlsserver() { _debug Le_Listen_V4 "$Le_Listen_V4" _debug Le_Listen_V6 "$Le_Listen_V6" - #openssl s_server binds a single socket, so both options together can only - #mean: do not force a family, same as when neither of them is given. - if [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then + if [ "$Le_Listen_V4" ]; then __S_OPENSSL="$__S_OPENSSL -4" - elif [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then + elif [ "$Le_Listen_V6" ]; then __S_OPENSSL="$__S_OPENSSL -6" fi @@ -3003,45 +2712,14 @@ __initHome() { _debug "Using default home: $DEFAULT_INSTALL_HOME" LE_WORKING_DIR="$DEFAULT_INSTALL_HOME" fi - # Convert a relative --home to an absolute path: later code cd's around - # (e.g. installOnline extracts and enters the archive dir), where a - # relative path would point into the wrong directory. - # https://github.com/acmesh-official/acme.sh/issues/6477 - case "$LE_WORKING_DIR" in - /*) ;; - *) - if [ -d "$LE_WORKING_DIR" ]; then - LE_WORKING_DIR="$(cd "$LE_WORKING_DIR" && pwd)" - fi - ;; - esac export LE_WORKING_DIR if [ -z "$LE_CONFIG_HOME" ]; then LE_CONFIG_HOME="$LE_WORKING_DIR" fi - case "$LE_CONFIG_HOME" in - /*) ;; - *) - if [ -d "$LE_CONFIG_HOME" ]; then - LE_CONFIG_HOME="$(cd "$LE_CONFIG_HOME" && pwd)" - fi - ;; - esac _debug "Using config home: $LE_CONFIG_HOME" export LE_CONFIG_HOME - # Paths with whitespace break the unquoted $_CURL/$_WGET command expansion, - # so fail early with a clear error instead of a cryptic curl/wget failure. - # https://github.com/acmesh-official/acme.sh/issues/2163 - case "$LE_WORKING_DIR$LE_CONFIG_HOME" in - *" "*) - _err "The --home or --config-home path can not contain spaces: '$LE_WORKING_DIR'" - _err "Please install $PROJECT_NAME to a path without spaces." - exit 1 - ;; - esac - _DEFAULT_ACCOUNT_CONF_PATH="$LE_CONFIG_HOME/account.conf" if [ -z "$ACCOUNT_CONF_PATH" ]; then @@ -3071,7 +2749,6 @@ _clearAPI() { ACME_REVOKE_CERT="" ACME_NEW_NONCE="" ACME_AGREEMENT="" - ACME_RENEWAL_INFO="" } #server @@ -3084,7 +2761,7 @@ _initAPI() { _request_retry_times=0 while [ -z "$ACME_NEW_ACCOUNT" ] && [ "${_request_retry_times}" -lt "$MAX_API_RETRY_TIMES" ]; do _request_retry_times=$(_math "$_request_retry_times" + 1) - response=$(_get "$_api_server" "" 10) + response=$(_get "$_api_server") if [ "$?" != "0" ]; then _debug2 "response" "$response" _info "Cannot init API for: $_api_server." @@ -3116,9 +2793,6 @@ _initAPI() { ACME_AGREEMENT=$(echo "$response" | _egrep_o 'termsOfService" *: *"[^"]*"' | cut -d '"' -f 3) export ACME_AGREEMENT - ACME_RENEWAL_INFO=$(echo "$response" | _egrep_o 'renewalInfo" *: *"[^"]*"' | cut -d '"' -f 3) - export ACME_RENEWAL_INFO - _debug "ACME_KEY_CHANGE" "$ACME_KEY_CHANGE" _debug "ACME_NEW_AUTHZ" "$ACME_NEW_AUTHZ" _debug "ACME_NEW_ORDER" "$ACME_NEW_ORDER" @@ -3126,7 +2800,6 @@ _initAPI() { _debug "ACME_REVOKE_CERT" "$ACME_REVOKE_CERT" _debug "ACME_AGREEMENT" "$ACME_AGREEMENT" _debug "ACME_NEW_NONCE" "$ACME_NEW_NONCE" - _debug "ACME_RENEWAL_INFO" "$ACME_RENEWAL_INFO" if [ "$ACME_NEW_ACCOUNT" ] && [ "$ACME_NEW_ORDER" ]; then return 0 fi @@ -3137,11 +2810,6 @@ _initAPI() { return 0 fi _err "Cannot init API for $_api_server" - if [ "$_api_server" = "$CA_ZEROSSL" ]; then - _info "$(__green "If this host is IPv6-only: ZeroSSL currently has no IPv6 endpoint.")" - _info "$(__green "Try another CA, e.g.: $PROJECT_ENTRY --set-default-ca --server letsencrypt")" - _info "See: $(__green "https://github.com/acmesh-official/acme.sh/issues/6872")" - fi return 1 } @@ -3572,14 +3240,9 @@ _setNginx() { fi echo "$NGINX_START -location ^~ /.well-known/acme-challenge/ { - # the ^~ prefix wins over regex-skipping blocks like \"location ^~ /\", - # the nested regex location still captures the token as \$1 - location ~ \"^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)\$\" { - default_type text/plain; - return 200 \"\$1.$_thumbpt\"; - } - return 404; +location ~ \"^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)\$\" { + default_type text/plain; + return 200 \"\$1.$_thumbpt\"; } #NGINX_START " >>"$FOUND_REAL_NGINX_CONF" @@ -3723,7 +3386,7 @@ _restoreNginx() { done _info "Reloading nginx" - if ! nginx -s reload >/dev/null 2>&1; then + if ! nginx -s reload >/dev/null; then _err "An error occurred while reloading nginx, please open an issue on $PROJECT." return 1 fi @@ -3844,7 +3507,7 @@ _on_before_issue() { _debug _chk_alt_domains "$_chk_alt_domains" #run pre hook if [ "$_chk_pre_hook" ]; then - _info "Running pre hook:'$_chk_pre_hook'" + _info "Runing pre hook:'$_chk_pre_hook'" if ! ( export Le_Domain="$_chk_main_domain" export Le_Alt="$_chk_alt_domains" @@ -3855,9 +3518,9 @@ _on_before_issue() { fi fi - if _hasfield "$_chk_web_roots" "$NO_VALUE" && [ "$_chk_web_roots" = "$NO_VALUE" ]; then - if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then - _err "Please install socat or python tools first." + if _hasfield "$_chk_web_roots" "$NO_VALUE"; then + if ! _exists "socat"; then + _err "Please install socat tools first." return 1 fi fi @@ -4027,10 +3690,10 @@ _on_issue_success() { #account_key_length eab-kid eab-hmac-key registeraccount() { _account_key_length="$1" - _eab_kid="$2" + _eab_id="$2" _eab_hmac_key="$3" _initpath - _regAccount "$_account_key_length" "$_eab_kid" "$_eab_hmac_key" + _regAccount "$_account_key_length" "$_eab_id" "$_eab_hmac_key" } __calcAccountKeyHash() { @@ -4041,16 +3704,6 @@ __calc_account_thumbprint() { printf "%s" "$jwk" | tr -d ' ' | _digest "sha256" | _url_replace } -#Reads a comma- or space-separated email list from stdin and prints -#the ACME contact list items: "mailto:a@example.com","mailto:b@example.com" -_mailto_contacts() { - _mc_out="" - for _mc_m in $(tr ',' ' '); do - _mc_out="$_mc_out,\"mailto:$_mc_m\"" - done - echo "$_mc_out" | cut -c 2- -} - _getAccountEmail() { if [ "$ACCOUNT_EMAIL" ]; then echo "$ACCOUNT_EMAIL" @@ -4070,14 +3723,14 @@ _getAccountEmail() { _regAccount() { _initpath _reg_length="$1" - _eab_kid="$2" + _eab_id="$2" _eab_hmac_key="$3" _debug3 _regAccount "$_regAccount" _initAPI mkdir -p "$CA_DIR" - if [ ! -s "$ACCOUNT_KEY_PATH" ]; then + if [ ! -f "$ACCOUNT_KEY_PATH" ]; then if ! _create_account_key "$_reg_length"; then _err "Error creating account key." return 1 @@ -4087,13 +3740,13 @@ _regAccount() { if ! _calcjwk "$ACCOUNT_KEY_PATH"; then return 1 fi - if [ "$_eab_kid" ] && [ "$_eab_hmac_key" ]; then - _savecaconf CA_EAB_KEY_ID "$_eab_kid" + if [ "$_eab_id" ] && [ "$_eab_hmac_key" ]; then + _savecaconf CA_EAB_KEY_ID "$_eab_id" _savecaconf CA_EAB_HMAC_KEY "$_eab_hmac_key" fi - _eab_kid=$(_readcaconf "CA_EAB_KEY_ID") + _eab_id=$(_readcaconf "CA_EAB_KEY_ID") _eab_hmac_key=$(_readcaconf "CA_EAB_HMAC_KEY") - _secure_debug3 _eab_kid "$_eab_kid" + _secure_debug3 _eab_id "$_eab_id" _secure_debug3 _eab_hmac_key "$_eab_hmac_key" _email="$(_getAccountEmail)" if [ "$_email" ]; then @@ -4101,7 +3754,7 @@ _regAccount() { fi if [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then - if [ -z "$_eab_kid" ] || [ -z "$_eab_hmac_key" ]; then + if [ -z "$_eab_id" ] || [ -z "$_eab_hmac_key" ]; then _info "No EAB credentials found for ZeroSSL, let's obtain them" if [ -z "$_email" ]; then _info "$(__green "$PROJECT_NAME is using ZeroSSL as default CA now.")" @@ -4110,19 +3763,17 @@ _regAccount() { _info "See: $(__green "$_ZEROSSL_WIKI")" return 1 fi - #the ZeroSSL EAB endpoint takes a single address, use the first one - _eab_email="$(echo "$_email" | tr ',' ' ' | awk '{print $1}')" - _eabresp=$(_post "email=$_eab_email" $_ZERO_EAB_ENDPOINT) + _eabresp=$(_post "email=$_email" $_ZERO_EAB_ENDPOINT) if [ "$?" != "0" ]; then _debug2 "$_eabresp" _err "Cannot get EAB credentials from ZeroSSL." return 1 fi _secure_debug2 _eabresp "$_eabresp" - _eab_kid="$(echo "$_eabresp" | tr ',}' '\n\n' | grep '"eab_kid"' | cut -d : -f 2 | tr -d '"')" - _secure_debug2 _eab_kid "$_eab_kid" - if [ -z "$_eab_kid" ]; then - _err "Cannot resolve _eab_kid" + _eab_id="$(echo "$_eabresp" | tr ',}' '\n\n' | grep '"eab_kid"' | cut -d : -f 2 | tr -d '"')" + _secure_debug2 _eab_id "$_eab_id" + if [ -z "$_eab_id" ]; then + _err "Cannot resolve _eab_id" return 1 fi _eab_hmac_key="$(echo "$_eabresp" | tr ',}' '\n\n' | grep '"eab_hmac_key"' | cut -d : -f 2 | tr -d '"')" @@ -4131,12 +3782,12 @@ _regAccount() { _err "Cannot resolve _eab_hmac_key" return 1 fi - _savecaconf CA_EAB_KEY_ID "$_eab_kid" + _savecaconf CA_EAB_KEY_ID "$_eab_id" _savecaconf CA_EAB_HMAC_KEY "$_eab_hmac_key" fi fi - if [ "$_eab_kid" ] && [ "$_eab_hmac_key" ]; then - eab_protected="{\"alg\":\"HS256\",\"kid\":\"$_eab_kid\",\"url\":\"${ACME_NEW_ACCOUNT}\"}" + if [ "$_eab_id" ] && [ "$_eab_hmac_key" ]; then + eab_protected="{\"alg\":\"HS256\",\"kid\":\"$_eab_id\",\"url\":\"${ACME_NEW_ACCOUNT}\"}" _debug3 eab_protected "$eab_protected" eab_protected64=$(printf "%s" "$eab_protected" | _base64 | _url_replace) @@ -4150,10 +3801,6 @@ _regAccount() { key_hex="$(_durl_replace_base64 "$_eab_hmac_key" | _dbase64 | _hex_dump | tr -d ' ')" _debug3 key_hex "$key_hex" - if [ -z "$key_hex" ]; then - _err "Cannot base64-decode the eab-hmac-key. Please check the value, and your openssl version." - return 1 - fi eab_signature=$(printf "%s" "$eab_sign_t" | _hmac sha256 $key_hex | _base64 | _url_replace) _debug3 eab_signature "$eab_signature" @@ -4162,7 +3809,7 @@ _regAccount() { _debug3 externalBinding "$externalBinding" fi if [ "$_email" ]; then - email_sg="\"contact\": [$(echo "$_email" | _mailto_contacts)], " + email_sg="\"contact\": [\"mailto:$_email\"], " fi regjson="{$email_sg\"termsOfServiceAgreed\": true$externalBinding}" @@ -4198,7 +3845,7 @@ _regAccount() { fi _savecaconf "ACCOUNT_URL" "$_accUri" else - _accUri="$(_readcaconf ACCOUNT_URL)" + ACCOUNT_URL="$(_readcaconf ACCOUNT_URL)" fi export ACCOUNT_URL="$_accUri" @@ -4206,9 +3853,7 @@ _regAccount() { _debug "Calc CA_KEY_HASH" "$CA_KEY_HASH" _savecaconf CA_KEY_HASH "$CA_KEY_HASH" - #RFC 8555 sec 7.3.6 requires 401 for requests from a deactivated account, - #but Boulder (Let's Encrypt) historically returns 403. Accept both. - if [ "$code" = '403' ] || [ "$code" = '401' ]; then + if [ "$code" = '403' ]; then _err "It seems that the account key has been deactivated, please use a new account key." return 1 fi @@ -4242,7 +3887,7 @@ updateaccount() { _email="$(_getAccountEmail)" if [ "$_email" ]; then - updjson='{"contact": ['$(echo "$_email" | _mailto_contacts)']}' + updjson='{"contact": ["mailto:'$_email'"]}' else updjson='{"contact": []}' fi @@ -4252,12 +3897,6 @@ updateaccount() { if [ "$code" = '200' ]; then echo "$response" >"$ACCOUNT_JSON_PATH" _info "Account update success for $_accUri." - # persist the effective mailbox like _regAccount does; otherwise - # "--update-account -m new@..." updates the CA but the local conf - # keeps showing the old address (issue 4673) - if [ "$_email" ]; then - _savecaconf "CA_EMAIL" "$_email" - fi ACCOUNT_THUMBPRINT="$(__calc_account_thumbprint)" _info "ACCOUNT_THUMBPRINT" "$ACCOUNT_THUMBPRINT" @@ -4267,93 +3906,6 @@ updateaccount() { fi } -#Implement account key rollover -updateaccountkey() { - _length="$1" - _initpath - - if [ ! -f "$ACCOUNT_KEY_PATH" ]; then - _err "Account key not found at: $ACCOUNT_KEY_PATH" - return 1 - fi - ACCOUNT_KEY_PATH_NEW="$ACCOUNT_KEY_PATH.new" - - _accUri=$(_readcaconf "ACCOUNT_URL") - _debug _accUri "$_accUri" - - if [ -z "$_accUri" ]; then - _err "The account URL is empty, please run '--update-account' first to update the account info, then try again." - return 1 - fi - if ! _calcjwk "$ACCOUNT_KEY_PATH"; then - return 1 - fi - _inner_payload="{\"account\": \"$_accUri\", \"oldKey\": $jwk}" - - _initAPI - if [ -z "$ACME_KEY_CHANGE" ]; then - _err "Server does not expose keyChange url." - return 1 - fi - - _url="$ACME_KEY_CHANGE" - if _createkey "$_length" "$ACCOUNT_KEY_PATH_NEW"; then - _info "New account key creation OK." - else - _err "New account key creation error." - return 1 - fi - - if ! _calcjwk "$ACCOUNT_KEY_PATH_NEW"; then - rm -f "$ACCOUNT_KEY_PATH_NEW" - return 1 - fi - _inner_protected="{\"url\": \"${_url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}' - _inner_protected64="$(printf "%s" "$_inner_protected" | _base64 | _url_replace)" - _inner_payload64="$(printf "%s" "$_inner_payload" | _base64 | _url_replace)" - if ! _inner_sig_t="$(printf "%s" "$_inner_protected64.$_inner_payload64" | _sign "$ACCOUNT_KEY_PATH_NEW" "sha256")"; then - _err "Sign request failed." - rm -f "$ACCOUNT_KEY_PATH_NEW" - return 1 - fi - _debug3 _inner_sig_t "$_inner_sig_t" - - _inner_sig="$(printf "%s" "$_inner_sig_t" | _url_replace)" - _debug3 _inner_sig "$_inner_sig" - - _body="{\"protected\": \"$_inner_protected64\", \"payload\": \"$_inner_payload64\", \"signature\": \"$_inner_sig\"}" - - if ! _send_signed_request "$_url" "$_body" "" "$ACCOUNT_KEY_PATH"; then - _err "Error rotating account key: $response." - rm -f "$ACCOUNT_KEY_PATH_NEW" - return 1 - fi - - if [ "$code" = '200' ]; then - echo "$response" >"$ACCOUNT_JSON_PATH" - mv -f "$ACCOUNT_KEY_PATH_NEW" "$ACCOUNT_KEY_PATH" - _info "Account key rotation success for $_accUri." - elif [ "$code" = "409" ]; then - _err "An existing account is using the new key" - rm -f "$ACCOUNT_KEY_PATH_NEW" - return 1 - else - _err "Account key rollover error: $response" - rm -f "$ACCOUNT_KEY_PATH_NEW" - return 1 - fi - - __CACHED_JWK_KEY_FILE="" - _calcjwk "$ACCOUNT_KEY_PATH" - - ACCOUNT_THUMBPRINT="$(__calc_account_thumbprint)" - _info "ACCOUNT_THUMBPRINT" "$ACCOUNT_THUMBPRINT" - - CA_KEY_HASH="$(__calcAccountKeyHash)" - _debug "Calc CA_KEY_HASH" "$CA_KEY_HASH" - _savecaconf CA_KEY_HASH "$CA_KEY_HASH" -} - #Implement deactivate account deactivateaccount() { _initpath @@ -4381,8 +3933,7 @@ deactivateaccount() { if _send_signed_request "$_accUri" "$_djson" && _contains "$response" '"deactivated"'; then _info "Successfully deactivated account $_accUri." _accid=$(echo "$response" | _egrep_o "\"id\" *: *[^,]*," | cut -d : -f 2 | tr -d ' ,') - elif [ "$code" = "403" ] || [ "$code" = "401" ]; then - #RFC 8555 sec 7.3.6: 401 from a deactivated account; Boulder returns 403 + elif [ "$code" = "403" ]; then _info "The account is already deactivated." _accid=$(_getfield "$_accUri" "999" "/") else @@ -4408,134 +3959,6 @@ deactivateaccount() { fi } -#domain -#Print the Validation Domain Name where the persistent TXT record must be -#published: the "_validation-persist" label prepended to the domain being -#validated (draft-ietf-acme-dns-persist-01 sec 4). -#A wildcard identifier is validated by the record at its base domain, so the -#leading "*." label is dropped: the wildcard scope comes from 'policy=wildcard' -#in the record value, not from a "*" label in the record name (sec 5.1, 10.2). -_dns_persist_txt_name() { - _dpt_domain="$1" - if _startswith "$_dpt_domain" "*."; then - _dpt_domain="$(echo "$_dpt_domain" | sed 's/^\*\.//')" - fi - if [ -z "$_dpt_domain" ]; then - return 1 - fi - echo "_validation-persist.$_dpt_domain" -} - -#domain wildcard ca_name days -#Print the TXT record(s) the user must add to enable persistent DNS validation -#per draft-ietf-acme-dns-persist-01. -makednspersistvalue() { - _mdpv_domain="$1" - _mdpv_wildcard="$2" - _mdpv_ca_name="$3" - _mdpv_days="$4" - - if [ -z "$_mdpv_domain" ]; then - _err "Please specify a domain with -d." - return 1 - fi - - _txt_name="$(_dns_persist_txt_name "$_mdpv_domain")" - if [ -z "$_txt_name" ]; then - _err "Invalid domain: $_mdpv_domain" - return 1 - fi - _debug _txt_name "$_txt_name" - - #A wildcard identifier can only be issued if the record carries - #'policy=wildcard', so don't print a record that is guaranteed to fail. - if _startswith "$_mdpv_domain" "*." && [ "$_mdpv_wildcard" != "1" ]; then - _info "$_mdpv_domain is a wildcard domain, adding 'policy=wildcard' automatically." - _mdpv_wildcard="1" - fi - - if [ -n "$_mdpv_days" ]; then - case "$_mdpv_days" in - '' | *[!0-9]*) - _err "--dns-persist-days must be a positive integer, got: $_mdpv_days" - return 1 - ;; - esac - if [ "$_mdpv_days" -lt 1 ]; then - _err "--dns-persist-days must be at least 1." - return 1 - fi - fi - - _initpath - - _accUri="$(_readcaconf ACCOUNT_URL)" - if [ -z "$_accUri" ]; then - _info "No account is registered for $ACME_DIRECTORY yet, registering one now..." - if ! _regAccount "$DEFAULT_ACCOUNT_KEY_LENGTH"; then - _err "Cannot register account." - return 1 - fi - _accUri="$(_readcaconf ACCOUNT_URL)" - fi - - if [ -z "$_accUri" ]; then - _err "Cannot determine the ACME account URL." - return 1 - fi - _debug "Account URL" "$_accUri" - - _txt_suffix="; accounturi=$_accUri" - if [ "$_mdpv_wildcard" = "1" ]; then - _txt_suffix="$_txt_suffix; policy=wildcard" - fi - if [ -n "$_mdpv_days" ]; then - _persist_until=$(_math "$(_time)" + "$_mdpv_days" \* 86400) - _txt_suffix="$_txt_suffix; persistUntil=$_persist_until" - _info "persistUntil set to $(__green "$(_time2str "$_persist_until")") ($_mdpv_days days from now)" - fi - - if [ -n "$_mdpv_ca_name" ]; then - _info "" - _info "Add the following DNS TXT record to enable persistent DNS validation:" - _info "" - _info "$(printf 'TXT persist domain:%s' "$(__green "$_txt_name")")" - _info "$(printf 'TXT persist value :%s' "$(__green "\"$_mdpv_ca_name$_txt_suffix\"")")" - _info "" - return 0 - fi - - _info "Fetching ACME directory: $ACME_DIRECTORY" - _dir_resp="$(_get "$ACME_DIRECTORY" "" 30)" - if [ "$?" != "0" ] || [ -z "$_dir_resp" ]; then - _err "Cannot fetch ACME directory: $ACME_DIRECTORY" - return 1 - fi - _dir_resp="$(echo "$_dir_resp" | _json_decode)" - _debug2 _dir_resp "$_dir_resp" - - _caa_array="$(echo "$_dir_resp" | tr -d ' \r\n\t' | _egrep_o '"caaIdentities":\[[^]]*\]')" - _debug2 _caa_array "$_caa_array" - _caaids="$(echo "$_caa_array" | sed 's/.*\[//' | sed 's/\].*//' | tr ',' '\n' | tr -d '"')" - _debug2 _caaids "$_caaids" - - if [ -z "$_caaids" ]; then - _err "The directory does not include 'caaIdentities'. Please specify --dns-persist-ca-name explicitly." - return 1 - fi - - _info "" - _info "Add ANY ONE of the following DNS TXT records to enable persistent DNS validation." - _info "(You only need to add one; pick whichever issuer identity you prefer.)" - for _id in $_caaids; do - [ -z "$_id" ] && continue - _info "" - _info "$(printf 'TXT persist domain:%s' "$(__green "$_txt_name")")" - _info "$(printf 'TXT persist value :%s' "$(__green "\"$_id$_txt_suffix\"")")" - done - _info "" -} - # domain folder file _findHook() { _hookdomain="$1" @@ -4885,25 +4308,16 @@ _match_issuer() { #ip _isIPv4() { - #splitting must not glob: a "*" segment would match files in cwd - set -f - _ipv4_saved_ifs="$IFS" - IFS='.' - # shellcheck disable=SC2086 - set -- $1 - IFS="$_ipv4_saved_ifs" - set +f - if [ $# -ne 4 ]; then - return 1 - fi - for _ipv4_seg in "$@"; do - _debug2 _ipv4_seg "$_ipv4_seg" - case "$_ipv4_seg" in - *[!0-9]* | "") return 1 ;; - esac - if [ "${#_ipv4_seg}" -gt 3 ] || [ "$_ipv4_seg" -gt 255 ]; then + for seg in $(echo "$1" | tr '.' ' '); do + _debug2 seg "$seg" + if [ "$(echo "$seg" | tr -d '[0-9]')" ]; then + #not all number return 1 fi + if [ $seg -ge 0 ] && [ $seg -lt 256 ]; then + continue + fi + return 1 done return 0 } @@ -4996,24 +4410,15 @@ issue() { _preferred_chain="${15}" _valid_from="${16}" _valid_to="${17}" - _certificate_profile="${18}" - _extended_key_usage="${19}" if [ -z "$_ACME_IS_RENEW" ]; then _initpath "$_main_domain" "$_key_length" mkdir -p "$DOMAIN_PATH" - elif [ -z "$Le_Vlist" ]; then - # Whether the saved order is resumed is decided by Le_Vlist below, so key - # this on Le_Vlist too. With no pending order to resume a new one is - # created, and a stale order link from the previous issuance must not be - # reused. https://github.com/acmesh-official/acme.sh/issues/3635 + elif ! _hasfield "$_web_roots" "$W_DNS"; then Le_OrderFinalize="" Le_LinkOrder="" + Le_LinkCert="" fi - # Per-run state only: it is set after finalize and never read back from the - # saved domain conf. Carrying it over would make a run that gives up while - # the order is still 'processing' download the previous certificate again. - Le_LinkCert="" if _hasfield "$_web_roots" "$W_DNS" && [ -z "$FORCE_DNS_MANUAL" ]; then _err "$_DNS_MANUAL_ERROR" @@ -5023,8 +4428,8 @@ issue() { if [ -f "$DOMAIN_CONF" ]; then Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime) _debug Le_NextRenewTime "$Le_NextRenewTime" - if [ -z "$FORCE" ] && [ -z "$_ari_should_renew" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then - _valid_to_saved=$(_readdomainconf Le_Valid_To) + if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + _valid_to_saved=$(_readdomainconf Le_Valid_to) if [ "$_valid_to_saved" ] && ! _startswith "$_valid_to_saved" "+"; then _info "The domain is set to be valid to: $_valid_to_saved" _info "It cannot be renewed automatically" @@ -5075,23 +4480,11 @@ issue() { else _cleardomainconf "Le_ChallengeAlias" fi - # Save Le_DNSSleep unconditionally here: the save inside the dns_entries - # branch is skipped when all authorizations are already valid (e.g. issuing - # the ECC twin of a just-issued RSA cert), which left the setting out of - # that cert's conf. https://github.com/acmesh-official/acme.sh/issues/6986 - if [ "$Le_DNSSleep" ]; then - _savedomainconf "Le_DNSSleep" "$Le_DNSSleep" - fi if [ "$_preferred_chain" ]; then _savedomainconf "Le_Preferred_Chain" "$_preferred_chain" "base64" else _cleardomainconf "Le_Preferred_Chain" fi - if [ "$_certificate_profile" ]; then - _savedomainconf "Le_Certificate_Profile" "$_certificate_profile" - else - _cleardomainconf "Le_Certificate_Profile" - fi Le_API="$ACME_DIRECTORY" _savedomainconf "Le_API" "$Le_API" @@ -5103,7 +4496,6 @@ issue() { if ! _on_before_issue "$_web_roots" "$_main_domain" "$_alt_domains" "$_pre_hook" "$_local_addr"; then _err "_on_before_issue." - _on_issue_err "$_post_hook" return 1 fi @@ -5111,7 +4503,7 @@ issue() { _debug2 _saved_account_key_hash "$_saved_account_key_hash" if [ -z "$ACCOUNT_URL" ] || [ -z "$_saved_account_key_hash" ] || [ "$_saved_account_key_hash" != "$(__calcAccountKeyHash)" ]; then - if ! _regAccount "$_accountkeylength" "$_eab_kid" "$_eab_hmac_key"; then + if ! _regAccount "$_accountkeylength"; then _on_issue_err "$_post_hook" return 1 fi @@ -5156,25 +4548,12 @@ issue() { return 1 fi fi - _keyusage="$_extended_key_usage" - if [ "$Le_API" = "$CA_GOOGLE" ] || [ "$Le_API" = "$CA_GOOGLE_TEST" ]; then - if [ -z "$_keyusage" ]; then - #https://github.com/acmesh-official/acme.sh/issues/6610 - #google accepts serverauth only - _keyusage="serverAuth" - fi - fi - if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_keyusage"; then + if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then _err "Error creating CSR." _clearup _on_issue_err "$_post_hook" return 1 fi - if [ "$_extended_key_usage" ]; then - _savedomainconf "Le_ExtKeyUse" "$_extended_key_usage" - else - _cleardomainconf "Le_ExtKeyUse" - fi fi _savedomainconf "Le_Keylength" "$_key_length" @@ -5237,48 +4616,13 @@ issue() { if [ "$_notAfter" ]; then _newOrderObj="$_newOrderObj,\"notAfter\": \"$_notAfter\"" fi - if [ "$_certificate_profile" ]; then - _newOrderObj="$_newOrderObj,\"profile\": \"$_certificate_profile\"" - fi - - # RFC 9773 Section 5: include "replaces" only when this is an actual - # renewal (--renew path), the CA advertises renewalInfo, and a prior - # cert exists. --issue (even with --force) is not a renewal per RFC 9773 - # which speaks of "a clear predecessor certificate" issued by this CA. - # NO_ARI=1 (env, account.conf, or ca.conf) disables ARI entirely, so the - # "replaces" field is also omitted. - _replaces_certID="" - if [ "$NO_ARI" = "1" ]; then - _debug "NO_ARI=1, omitting ARI 'replaces' field from newOrder" - elif [ "$_ACME_IS_RENEW" = "1" ] && [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ]; then - _replaces_certID="$(_getARICertID "$CERT_PATH")" - _debug "Adding ARI replaces" "$_replaces_certID" - fi - _debug "STEP 1, Ordering a Certificate" - _newOrderReplacesObj="$_newOrderObj" - if [ "$_replaces_certID" ]; then - _newOrderReplacesObj="$_newOrderObj,\"replaces\": \"$_replaces_certID\"" - fi - if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderReplacesObj}"; then + if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then _err "Error creating new order." _clearup _on_issue_err "$_post_hook" return 1 fi - # RFC 9773 Section 5 only defines the "alreadyReplaced" error, but real CAs - # (Let's Encrypt) may also reject with a malformed error if the prior cert - # was issued by a different issuer / different CA. Retry without "replaces" - # whenever the failure mentions ARI or the replaces field. - if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "urn:ietf:params:acme:error:malformed" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then - _info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'." - if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then - _err "Error creating new order." - _clearup - _on_issue_err "$_post_hook" - return 1 - fi - fi if _contains "$response" "invalid"; then if echo "$response" | _normalizeJson | grep '"status":"invalid"' >/dev/null 2>&1; then _err "Create new order with invalid status." @@ -5303,7 +4647,7 @@ issue() { #for dns manual mode _savedomainconf "Le_OrderFinalize" "$Le_OrderFinalize" - _authorizations_seg="$(echo "$response" | _json_decode | _authorizations_from_order)" + _authorizations_seg="$(echo "$response" | _json_decode | _egrep_o '"authorizations" *: *\[[^\[]*\]' | cut -d '[' -f 2 | tr -d ']' | tr -d '"')" _debug2 _authorizations_seg "$_authorizations_seg" if [ -z "$_authorizations_seg" ]; then _err "_authorizations_seg not found." @@ -5369,9 +4713,7 @@ $_authorizations_map" vtype="$VTYPE_HTTP" #todo, v2 wildcard force to use dns - if [ "$_currentRoot" = "$W_DNS_PERSIST" ]; then - vtype="$VTYPE_DNS_PERSIST" - elif _startswith "$_currentRoot" "$W_DNS"; then + if _startswith "$_currentRoot" "$W_DNS"; then vtype="$VTYPE_DNS" fi @@ -5413,8 +4755,7 @@ $_authorizations_map" _debug keyauthorization "$keyauthorization" fi - # Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018 - entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^{]*"type":"'$vtype'"[^}]*')" + entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$keyauthorization" -a -z "$entry" ]; then @@ -5429,7 +4770,18 @@ $_authorizations_map" fi if [ -z "$keyauthorization" ]; then + token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" + _debug token "$token" + + if [ -z "$token" ]; then + _err "Cannot get domain token $entry" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + uri="$(echo "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)" + _debug uri "$uri" if [ -z "$uri" ]; then @@ -5438,26 +4790,8 @@ $_authorizations_map" _on_issue_err "$_post_hook" return 1 fi - - if [ "$vtype" = "$VTYPE_DNS_PERSIST" ]; then - # dns-persist-01 challenges have no token; the TXT record is - # provisioned out-of-band. Use a non-empty placeholder so the - # downstream code does not treat this entry as already verified. - keyauthorization="$VTYPE_DNS_PERSIST" - _debug keyauthorization "$keyauthorization" - else - token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" - _debug token "$token" - - if [ -z "$token" ]; then - _err "Cannot get domain token $entry" - _clearup - _on_issue_err "$_post_hook" - return 1 - fi - keyauthorization="$token.$thumbprint" - _debug keyauthorization "$keyauthorization" - fi + keyauthorization="$token.$thumbprint" + _debug keyauthorization "$keyauthorization" fi dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot$sep$_authz_url" @@ -5493,8 +4827,6 @@ $_authorizations_map" fi _d_alias="$(_getfield "$_challenge_alias" "$_alias_index")" test "$_d_alias" = "$NO_VALUE" && _d_alias="" - # strip the trailing dot of a fully-qualified alias domain - _d_alias="${_d_alias%.}" _alias_index="$(_math "$_alias_index" + 1)" _debug "_d_alias" "$_d_alias" if [ "$_d_alias" ]; then @@ -5689,8 +5021,6 @@ $_authorizations_map" fi fi elif [ "$vtype" = "$VTYPE_ALPN" ]; then - _ncaddr="$(_getfield "$_local_addr" "$_ncIndex")" - _ncIndex="$(_math $_ncIndex + 1)" acmevalidationv1="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")" _debug acmevalidationv1 "$acmevalidationv1" if ! _starttlsserver "$d" "" "$Le_TLSPort" "$keyauthorization" "$_ncaddr" "$acmevalidationv1"; then @@ -5746,7 +5076,7 @@ $_authorizations_map" status=$(echo "$response" | _egrep_o '"status":"[^"]*' | cut -d : -f 2 | tr -d '"') _debug2 status "$status" if _contains "$status" "invalid"; then - error="$(echo "$response" | _egrep_o '"error":[{][^}]*')" + error="$(echo "$response" | _egrep_o '"error":\{[^\}]*')" _debug2 error "$error" errordetail="$(echo "$error" | _egrep_o '"detail": *"[^"]*' | cut -d '"' -f 4)" _debug2 errordetail "$errordetail" @@ -5758,12 +5088,7 @@ $_authorizations_map" if [ "$DEBUG" ]; then if [ "$vtype" = "$VTYPE_HTTP" ]; then _debug "Debug: GET token URL." - if _isIPv6 "$d"; then - host="[$d]" - else - host="$d" - fi - _get "http://$host/.well-known/acme-challenge/$token" "" 1 + _get "http://$d/.well-known/acme-challenge/$token" "" 1 fi fi _clearupwebbroot "$_currentRoot" "$removelevel" "$token" @@ -5858,21 +5183,11 @@ $_authorizations_map" return 1 fi break - elif _contains "$response" "\"ready\""; then - _info "Order status is 'ready', let's sleep and retry." - _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') - _debug "_retryafter" "$_retryafter" - if [ "$_retryafter" ] && [ $_retryafter -gt 0 ]; then - _info "Sleeping for $_retryafter seconds then retrying" - _sleep $_retryafter - else - _sleep 2 - fi elif _contains "$response" "\"processing\""; then _info "Order status is 'processing', let's sleep and retry." _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') _debug "_retryafter" "$_retryafter" - if [ "$_retryafter" ] && [ $_retryafter -gt 0 ]; then + if [ "$_retryafter" ]; then _info "Sleeping for $_retryafter seconds then retrying" _sleep $_retryafter else @@ -5901,11 +5216,6 @@ $_authorizations_map" _link_cert_retry="$(_math $_link_cert_retry + 1)" done - # cover case where the final poll returned 'valid' - if [ -z "$Le_LinkCert" ] && _contains "$response" "\"status\":\"valid\""; then - Le_LinkCert="$(echo "$response" | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)" - fi - if [ -z "$Le_LinkCert" ]; then _err "Signing failed. Could not get Le_LinkCert, and stopped retrying after reaching the retry limit." _err "$response" @@ -5921,14 +5231,7 @@ $_authorizations_map" return 1 fi - if ! _contains "$response" "$BEGIN_CERT"; then - response="$(echo "$response" | _dbase64 "multiline" | tr -d '\0' | _normalizeJson)" - _err "Signing failed: $(echo "$response" | _egrep_o '"detail":"[^"]*"')" - _on_issue_err "$_post_hook" - return 1 - fi - - echo "$response" | _strip_blank_lines >"$CERT_PATH" + echo "$response" >"$CERT_PATH" _split_cert_chain "$CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CA_CERT_PATH" if [ -z "$_preferred_chain" ]; then _preferred_chain=$(_readcaconf DEFAULT_PREFERRED_CHAIN) @@ -5947,15 +5250,10 @@ $_authorizations_map" _err "$response" continue fi - - if ! _contains "$response" "$BEGIN_CERT"; then - _debug2 "Skipping alternate cert link due to unexpected response format." - continue - fi _relcert="$CERT_PATH.alt" _relfullchain="$CERT_FULLCHAIN_PATH.alt" _relca="$CA_CERT_PATH.alt" - echo "$response" | _strip_blank_lines >"$_relcert" + echo "$response" >"$_relcert" _split_cert_chain "$_relcert" "$_relfullchain" "$_relca" if [ "$DEBUG" ]; then _debug "rel chain issuers: " "$(_get_chain_issuers "$_relfullchain")" @@ -6015,7 +5313,7 @@ $_authorizations_map" Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime") _savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr" - if [ -z "$Le_RenewalDays" ]; then + if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ]; then Le_RenewalDays="$DEFAULT_RENEW" else _savedomainconf "Le_RenewalDays" "$Le_RenewalDays" @@ -6039,17 +5337,12 @@ $_authorizations_map" _clearaccountconf "HTTPS_INSECURE" fi - if [ "$Le_Listen_V4" ] || [ "$Le_Listen_V6" ]; then - if [ "$Le_Listen_V4" ]; then - _savedomainconf "Le_Listen_V4" "$Le_Listen_V4" - else - _cleardomainconf Le_Listen_V4 - fi - if [ "$Le_Listen_V6" ]; then - _savedomainconf "Le_Listen_V6" "$Le_Listen_V6" - else - _cleardomainconf Le_Listen_V6 - fi + if [ "$Le_Listen_V4" ]; then + _savedomainconf "Le_Listen_V4" "$Le_Listen_V4" + _cleardomainconf Le_Listen_V6 + elif [ "$Le_Listen_V6" ]; then + _savedomainconf "Le_Listen_V6" "$Le_Listen_V6" + _cleardomainconf Le_Listen_V4 fi if [ "$Le_ForceNewDomainKey" = "1" ]; then @@ -6065,89 +5358,33 @@ $_authorizations_map" _info "It cannot be renewed automatically" _info "See: $_VALIDITY_WIKI" else - Le_NextRenewTime=$(_calc_validto_renew_time "$Le_NextRenewTime" "$Le_RenewalDays" "$(_time)") - Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") - fi - elif [ "$Le_RenewalDays" -lt "0" ]; then - _enddate_value=$(_enddate "$CERT_PATH") - if [ "$?" != "0" ] || [ -z "$_enddate_value" ]; then - _err "Failed to get certificate end date for $CERT_PATH" - return 1 - fi - - _endtime=$(_ssldate2time "$_enddate_value") - if [ "$?" != "0" ] || [ -z "$_endtime" ]; then - _err "Cannot parse _enddate_value: $_enddate_value" - return 1 - fi - Le_NextRenewTime=$(_math "$_endtime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) - Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") - else - _endtime_for_cap="" - _enddate_value=$(_enddate "$CERT_PATH") - if [ "$?" = "0" ] && [ "$_enddate_value" ]; then - _endtime_for_cap=$(_ssldate2time "$_enddate_value") - fi - Le_NextRenewTime=$(_calc_next_renew_time "$Le_CertCreateTime" "$Le_RenewalDays" "$_endtime_for_cap") - Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") - fi - - # RFC 9773 ARI: if the CA exposes renewalInfo, override Le_NextRenewTime - # with a time picked at random within the suggestedWindow. This both gives - # the CA full control over renewal scheduling and disperses renewals across - # the network so all clients don't hit the CA at the same instant. - # Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and fall back to - # the legacy time-based renewal calculation. - if [ "$NO_ARI" = "1" ]; then - _debug "NO_ARI=1, skipping ARI suggestedWindow override" - elif [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ] && [ -z "$_notAfter" ]; then - _ari_resp_new="$(_get_ARI "$CERT_PATH")" - _debug2 "_ari_resp_new" "$_ari_resp_new" - _ari_start_new="$(echo "$_ari_resp_new" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')" - _ari_end_new="$(echo "$_ari_resp_new" | _egrep_o '"end" *: *"[^"]*' | sed 's/.*"//')" - if [ "$_ari_start_new" ] && [ "$_ari_end_new" ]; then - _ari_start_t_new="$(_date2time "$(echo "$_ari_start_new" | sed 's/\.[0-9]*//')")" - _ari_end_t_new="$(_date2time "$(echo "$_ari_end_new" | sed 's/\.[0-9]*//')")" - if [ "$_ari_start_t_new" ] && [ "$_ari_end_t_new" ] && [ "$_ari_end_t_new" -gt "$_ari_start_t_new" ]; then - _ari_window=$(_math "$_ari_end_t_new" - "$_ari_start_t_new") - _ari_offset=$(_math "$(_time)" % "$_ari_window") - Le_NextRenewTime=$(_math "$_ari_start_t_new" + "$_ari_offset") + _now=$(_time) + _debug2 "_now" "$_now" + _lifetime=$(_math $Le_NextRenewTime - $_now) + _debug2 "_lifetime" "$_lifetime" + if [ $_lifetime -gt 86400 ]; then + #if lifetime is logner than one day, it will renew one day before + Le_NextRenewTime=$(_math $Le_NextRenewTime - 86400) + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + else + #if lifetime is less than 24 hours, it will renew one hour before + Le_NextRenewTime=$(_math $Le_NextRenewTime - 3600) Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") - _info "ARI suggestedWindow: $(__green "$_ari_start_new") to $(__green "$_ari_end_new")" - _info "Next renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")" fi fi + else + Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) + Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") fi - - # Warn when the scheduled renewal falls after the cert has already expired, - # e.g. a 1-day cert from an internal CA combined with the default 30-day - # schedule, which computes from the creation date and never looks at - # notAfter. Skip the warning for a fixed-date --valid-to: there - # Le_NextRenewTime equals the expiry by design and the non-renewable state - # was already reported above. https://github.com/acmesh-official/acme.sh/issues/6917 - if [ -z "$_valid_to" ] || _startswith "$_valid_to" "+"; then - _renew_chk_enddate="$(_enddate "$CERT_PATH")" - _renew_chk_endtime="$(_ssldate2time "$_renew_chk_enddate")" - if [ "$Le_NextRenewTime" ] && [ "$_renew_chk_endtime" ] && [ "$Le_NextRenewTime" -ge "$_renew_chk_endtime" ]; then - _info "$(__red "WARNING: the cert expires at $_renew_chk_enddate, BEFORE the next scheduled renewal time $Le_NextRenewTimeStr.")" - _info "$(__red "The cert will already be expired when the renewal runs. If your CA issues short-lived certs, use a negative --days value (e.g. --days -1) to renew relative to the expiry time.")" - fi - fi - _savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr" _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime" #convert to pkcs12 - Le_PFXPassword="$(_readdomainconf Le_PFXPassword)" if [ "$Le_PFXPassword" ]; then _toPkcs "$CERT_PFX_PATH" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$Le_PFXPassword" fi - - #convert to pkcs8 - Le_PKCS8Password="$(_readdomainconf Le_PKCS8Password)" - if [ "$Le_PKCS8Password" ]; then - _toPkcs8 "$CERT_PKCS8_PATH" "$CERT_KEY_PATH" "$Le_PKCS8Password" - fi + export CERT_PFX_PATH if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then _savedomainconf "Le_RealCertPath" "$_real_cert" @@ -6167,18 +5404,6 @@ $_authorizations_map" } #in_out_cert out_fullchain out_ca -#Reads a PEM chain from stdin, prints it without the blank lines. -#Some CAs (Let's Encrypt) separate the certificates of a chain with a blank -#line, others (ZeroSSL) don't. The blank lines are valid PEM (RFC 7468), but -#some devices and APIs reject them, so the certs are stored back to back. -#https://github.com/acmesh-official/acme.sh/issues/1940 -_strip_blank_lines() { - #spell out space and tab: Solaris sed treats [[:space:]] as a literal - #bracket set and silently stops matching the blank lines - _sbl_tab="$(printf '\t')" - sed "/^[ $_sbl_tab]*\$/d" -} - _split_cert_chain() { _certf="$1" _fullchainf="$2" @@ -6207,7 +5432,7 @@ renew() { _debug "_renewServer" "$_renewServer" _initpath "$Le_Domain" "$_isEcc" - _info "Renew: $Le_Domain" + _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _info "$(__green "Renewing: '$Le_Domain'")" if [ ! -f "$DOMAIN_CONF" ]; then @@ -6222,31 +5447,26 @@ renew() { . "$DOMAIN_CONF" _debug Le_API "$Le_API" - #don't switch it back - # case "$Le_API" in - # "$CA_LETSENCRYPT_V2_TEST") - # _info "Switching back to $CA_LETSENCRYPT_V2" - # Le_API="$CA_LETSENCRYPT_V2" - # ;; - # "$CA_GOOGLE_TEST") - # _info "Switching back to $CA_GOOGLE" - # Le_API="$CA_GOOGLE" - # ;; - # esac + case "$Le_API" in + "$CA_LETSENCRYPT_V2_TEST") + _info "Switching back to $CA_LETSENCRYPT_V2" + Le_API="$CA_LETSENCRYPT_V2" + ;; + "$CA_BUYPASS_TEST") + _info "Switching back to $CA_BUYPASS" + Le_API="$CA_BUYPASS" + ;; + "$CA_GOOGLE_TEST") + _info "Switching back to $CA_GOOGLE" + Le_API="$CA_GOOGLE" + ;; + esac if [ "$_server" ]; then Le_API="$_server" fi _info "Renewing using Le_API=$Le_API" - # Honor --local-address given on the renew/renewAll command line: it overrides - # the value saved at issue time (and gets re-saved by issue() below), so certs - # issued before the machine gained multiple addresses can still be renewed. - # https://github.com/acmesh-official/acme.sh/issues/7009 - if [ "$_local_address" ]; then - Le_LocalAddress="$_local_address" - fi - _clearAPI _clearCA export ACME_DIRECTORY="$Le_API" @@ -6255,77 +5475,7 @@ renew() { _debug2 "initpath again." _initpath "$Le_Domain" "$_isEcc" - # ARI (RFC 9773): fetch the CA's suggestedWindow on every renewal check. - # If the window has started, renew now even if Le_NextRenewTime is in the future. - # Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and use only - # Le_NextRenewTime for the renewal decision. - if [ "$NO_ARI" = "1" ]; then - _debug "NO_ARI=1, skipping ARI suggestedWindow check" - elif [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then - if _initAPI && [ "$ACME_RENEWAL_INFO" ]; then - _ari_resp="$(_get_ARI "$CERT_PATH")" - _debug2 "_ari_resp" "$_ari_resp" - _ari_start="$(echo "$_ari_resp" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')" - _ari_end="$(echo "$_ari_resp" | _egrep_o '"end" *: *"[^"]*' | sed 's/.*"//')" - _debug "ARI suggestedWindow.start" "$_ari_start" - _debug "ARI suggestedWindow.end" "$_ari_end" - if [ "$_ari_start" ] && [ "$_ari_end" ]; then - _ari_start_t="$(_date2time "$(echo "$_ari_start" | sed 's/\.[0-9]*//')")" - _ari_end_t="$(_date2time "$(echo "$_ari_end" | sed 's/\.[0-9]*//')")" - _ari_explanation_url="$(echo "$_ari_resp" | _egrep_o '"explanationURL" *: *"[^"]*' | sed 's/.*"//')" - _debug "_ari_start_t" "$_ari_start_t" - _debug "_ari_end_t" "$_ari_end_t" - _debug "_ari_explanation_url" "$_ari_explanation_url" - _debug "Le_NextRenewTime" "$Le_NextRenewTime" - # Update ARI if needed - if [ "$_ari_start_t" ] && [ "$_ari_end_t" ] && [ "$Le_NextRenewTime" ] && [ "$_ari_end_t" -gt "$_ari_start_t" ] && ([ "$Le_NextRenewTime" -lt "$_ari_start_t" ] || [ "$Le_NextRenewTime" -gt "$_ari_end_t" ]); then - _ari_old_time_str="$Le_NextRenewTimeStr" - _info "Current renewal time: $(__green "$_ari_old_time_str")" - _ari_window=$(_math "$_ari_end_t" - "$_ari_start_t") - _ari_offset=$(_math "$(_time)" % "$_ari_window") - Le_NextRenewTime=$(_math "$_ari_start_t" + "$_ari_offset") - Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") - _info "ARI suggestedWindow: $(__green "$_ari_start") to $(__green "$_ari_end")" - _info "Updating renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")" - _savedomainconf Le_NextRenewTime "$Le_NextRenewTime" - _savedomainconf Le_NextRenewTimeStr "$Le_NextRenewTimeStr" - fi - if [ "$Le_NextRenewTime" ] && [ "$(_time)" -ge "$Le_NextRenewTime" ]; then - _info "ARI suggested renewal has passed ($(__green "$Le_NextRenewTimeStr")), proceeding with renewal." - if [ "$_ari_explanation_url" ]; then - _info "For more information on this renewal: $(__green "$_ari_explanation_url")" - fi - fi - fi - fi - fi - if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then - _renew_retry_fixed="" - res="0" - _ensure_install "$Le_Domain" - res="$?" - if [ "$Le_DeployHook" ] && [ "$res" = "0" ]; then - _ensure_deploy "$Le_Domain" - res="$?" - fi - if [ "$res" != "0" ]; then - if [ -z "$_ACME_IN_RENEWALL" ]; then - if [ $_set_level -ge $NOTIFY_LEVEL_ERROR ]; then - _send_notify "Renew $Le_Domain error" "There is an error." "$NOTIFY_HOOK" 1 - fi - fi - return 1 - fi - if [ "$_renew_retry_fixed" ]; then - _info "Install/deploy retry succeeded, no renewal is needed." - if [ -z "$_ACME_IN_RENEWALL" ]; then - if [ $_set_level -ge $NOTIFY_LEVEL_RENEW ]; then - _send_notify "Renew $Le_Domain success" "Good, the cert install/deploy retry succeeded." "$NOTIFY_HOOK" 0 - fi - fi - return 0 - fi _info "Skipping. Next renewal time is: $(__green "$Le_NextRenewTimeStr")" _info "Add '$(__red '--force')' to force renewal." if [ -z "$_ACME_IN_RENEWALL" ]; then @@ -6347,11 +5497,6 @@ renew() { Le_PostHook="$(_readdomainconf Le_PostHook)" Le_RenewHook="$(_readdomainconf Le_RenewHook)" Le_Preferred_Chain="$(_readdomainconf Le_Preferred_Chain)" - Le_Certificate_Profile="$(_readdomainconf Le_Certificate_Profile)" - Le_Valid_From="$(_readdomainconf Le_Valid_From)" - Le_Valid_To="$(_readdomainconf Le_Valid_To)" - Le_ExtKeyUse="$(_readdomainconf Le_ExtKeyUse)" - # When renewing from an old version, the empty Le_Keylength means 2048. # Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over # time but an empty value implies 2048 specifically. @@ -6359,17 +5504,13 @@ renew() { if [ -z "$Le_Keylength" ]; then Le_Keylength=2048 fi - if [ "$CA_LETSENCRYPT_V2" = "$Le_API" ]; then - #letsencrypt doesn't support ocsp anymore - if [ "$Le_OCSP_Staple" ]; then - export Le_OCSP_Staple="" - _cleardomainconf Le_OCSP_Staple - fi - fi - issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" "$Le_ExtKeyUse" + issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" res="$?" + if [ "$res" != "0" ]; then + return "$res" + fi - if [ "$Le_DeployHook" ] && [ "$res" = "0" ]; then + if [ "$Le_DeployHook" ]; then _deploy "$Le_Domain" "$Le_DeployHook" res="$?" fi @@ -6409,11 +5550,7 @@ renewAll() { _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _debug "_set_level" "$_set_level" export _ACME_IN_RENEWALL=1 - if ! [ -d "$CERT_HOME" ]; then - _err "$CERT_HOME is not a directory, please check your configuration." - return 1 - fi - for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do + for di in "${CERT_HOME}"/*.*/; do _debug di "$di" if ! [ -d "$di" ]; then _debug "Not a directory, skipping: $di" @@ -6421,19 +5558,12 @@ renewAll() { fi d=$(basename "$di") _debug d "$d" - _d_ari="$di.ari" - _debug _d_ari "$_d_ari" ( if _endswith "$d" "$ECC_SUFFIX"; then _isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2) d=$(echo "$d" | cut -d "$ECC_SEP" -f 1) fi renew "$d" "$_isEcc" "$_server" - rc="$?" - if [ "$rc" = "0" ] && [ "$_ari_explanation_url" ]; then - echo "$_ari_explanation_url" >"$_d_ari" - fi - return $rc ) rc="$?" _debug "Return code: $rc" @@ -6448,13 +5578,8 @@ renewAll() { _send_notify "Renew $d success" "Good, the cert is renewed." "$NOTIFY_HOOK" 0 fi fi - _renewal_explanation="" - if [ -f "$_d_ari" ]; then - _renewal_explanation=" ($(cat "$_d_ari"))" - rm -f "$_d_ari" - fi - _success_msg="${_success_msg} $d$_renewal_explanation + _success_msg="${_success_msg} $d " elif [ "$rc" = "$RENEW_SKIP" ]; then if [ $_error_level -gt $NOTIFY_LEVEL_SKIP ]; then @@ -6523,9 +5648,6 @@ ${_skipped_msg} fi fi - if [ "$_TREAT_SKIP_AS_SUCCESS" ] && [ "$_ret" = "$RENEW_SKIP" ]; then - _ret=0 - fi return "$_ret" } @@ -6549,10 +5671,6 @@ signcsr() { _local_addr="${11}" _challenge_alias="${12}" _preferred_chain="${13}" - _valid_f="${14}" - _valid_t="${15}" - _cert_prof="${16}" - _en_key_usage="${17}" _csrsubj=$(_readSubjectFromCSR "$_csrfile") if [ "$?" != "0" ]; then @@ -6596,7 +5714,7 @@ signcsr() { _info "Copying CSR to: $CSR_PATH" cp "$_csrfile" "$CSR_PATH" - issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain" "$_valid_f" "$_valid_t" "$_cert_prof" "$_en_key_usage" + issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain" } @@ -6647,10 +5765,9 @@ list() { _sep="|" if [ "$_raw" ]; then if [ -z "$_domain" ]; then - printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Profile${_sep}CA${_sep}Created${_sep}Renew" + printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}CA${_sep}Created${_sep}Renew" fi - for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do - [ -d "$di" ] || continue + for di in "${CERT_HOME}"/*.*/; do d=$(basename "$di") _debug d "$d" ( @@ -6663,7 +5780,7 @@ list() { . "$DOMAIN_CONF" _ca="$(_getCAShortName "$Le_API")" if [ -z "$_domain" ]; then - printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$Le_Certificate_Profile${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr" + printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr" else if [ "$_domain" = "$d" ]; then cat "$DOMAIN_CONF" @@ -6682,48 +5799,6 @@ list() { } -list_profiles() { - _initpath - _initAPI - - _l_server_url="$ACME_DIRECTORY" - _l_server_name="$(_getCAShortName "$_l_server_url")" - _info "Fetching profiles from $_l_server_name ($_l_server_url)..." - - response=$(_get "$_l_server_url" "" 10) - if [ "$?" != "0" ]; then - _err "Failed to connect to CA directory: $_l_server_url" - return 1 - fi - - normalized_response=$(echo "$response" | _normalizeJson) - profiles_json=$(echo "$normalized_response" | _egrep_o '"profiles" *: *[{][^}]*[}]') - - if [ -z "$profiles_json" ]; then - _info "The CA '$_l_server_name' does not publish certificate profiles via its directory endpoint." - return 0 - fi - - # Strip the outer layer to get the key-value pairs - profiles_kv=$(echo "$profiles_json" | sed 's/"profiles" *: *{//' | sed 's/}$//' | tr ',' '\n') - - printf "\n%-15s %s\n" "name" "info" - printf -- "--------------------------------------------------------------------\n" - - _old_IFS="$IFS" - IFS=' -' - for pair in $profiles_kv; do - # Trim quotes and whitespace - _name=$(echo "$pair" | cut -d: -f1 | tr -d '" \t') - _info_url=$(echo "$pair" | cut -d: -f2- | sed 's/^ *//' | tr -d '"') - printf "%-15s %s\n" "$_name" "$_info_url" - done - IFS="$_old_IFS" - - return 0 -} - _deploy() { _d="$1" _hooks="$2" @@ -6759,45 +5834,6 @@ _deploy() { _info "$(__green Success)" fi done - - _deploy_success_time="$(_time)" - _savedomainconf "Le_DeploySuccessTime" "$_deploy_success_time" - _savedomainconf "Le_DeploySuccessTimeStr" "$(_time2str "$_deploy_success_time")" -} - -_ensure_deploy() { - _d="$1" - if [ -z "$Le_DeployHook" ]; then - return 0 - fi - if [ -z "$Le_CertCreateTime" ]; then - return 0 - fi - - _deploy_success_time="$(_readdomainconf Le_DeploySuccessTime)" - if [ -z "$_deploy_success_time" ]; then - _debug "Le_DeploySuccessTime is empty, skip deploy retry check." - return 0 - fi - case "$_deploy_success_time$Le_CertCreateTime" in - *[!0-9]*) - _debug "Le_DeploySuccessTime or Le_CertCreateTime is not a number, skip deploy retry check." - return 0 - ;; - esac - - if [ "$_deploy_success_time" -lt "$Le_CertCreateTime" ]; then - _info "The cert was created after the last successful deploy, retrying deploy hooks." - if _deploy "$_d" "$Le_DeployHook"; then - _info "Deploy retry succeeded." - _renew_retry_fixed=1 - return 0 - fi - _err "Deploy retry failed." - return 1 - fi - - return 0 } #domain hooks @@ -6818,13 +5854,7 @@ deploy() { fi _debug2 DOMAIN_CONF "$DOMAIN_CONF" - # The cert dir may exist without a domain conf (e.g. the conf was deleted, or - # the cert was placed here manually). Deploy can still proceed using env-provided - # settings, and _savedomainconf below will recreate the conf, so only source it - # when present instead of failing on a missing file. - if [ -f "$DOMAIN_CONF" ]; then - . "$DOMAIN_CONF" - fi + . "$DOMAIN_CONF" _savedomainconf Le_DeployHook "$_hooks" @@ -6957,54 +5987,9 @@ _installcert() { _info "$(__green "Reload successful")" else _err "Reload error for: $_main_domain" - return 1 fi fi - _installcert_success_time="$(_time)" - _savedomainconf "Le_InstallCertSuccessTime" "$_installcert_success_time" - _savedomainconf "Le_InstallCertSuccessTimeStr" "$(_time2str "$_installcert_success_time")" -} - -_ensure_install() { - _d="$1" - if [ -z "$Le_CertCreateTime" ]; then - return 0 - fi - - _real_cert="$(_readdomainconf Le_RealCertPath)" - _real_key="$(_readdomainconf Le_RealKeyPath)" - _real_ca="$(_readdomainconf Le_RealCACertPath)" - _reload_cmd="$(_readdomainconf Le_ReloadCmd)" - _real_fullchain="$(_readdomainconf Le_RealFullChainPath)" - if [ -z "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then - return 0 - fi - - _installcert_success_time="$(_readdomainconf Le_InstallCertSuccessTime)" - if [ -z "$_installcert_success_time" ]; then - _debug "Le_InstallCertSuccessTime is empty, skip install retry check." - return 0 - fi - case "$_installcert_success_time$Le_CertCreateTime" in - *[!0-9]*) - _debug "Le_InstallCertSuccessTime or Le_CertCreateTime is not a number, skip install retry check." - return 0 - ;; - esac - - if [ "$_installcert_success_time" -lt "$Le_CertCreateTime" ]; then - _info "The cert was created after the last successful install, retrying install cert." - if _installcert "$_d" "$_real_cert" "$_real_key" "$_real_ca" "$_real_fullchain" "$_reload_cmd"; then - _info "Install cert retry succeeded." - _renew_retry_fixed=1 - return 0 - fi - _err "Install cert retry failed." - return 1 - fi - - return 0 } __read_password() { @@ -7024,7 +6009,6 @@ _install_win_taskscheduler() { _lesh="$1" _centry="$2" _randomminute="$3" - _randomhour="$4" if ! _exists cygpath; then _err "cygpath not found" return 1 @@ -7051,10 +6035,8 @@ _install_win_taskscheduler() { _info "$PROJECT_NAME will not save your password." _info "Please input your Windows password for: $(__green "$_myname")" _password="$(__read_password)" - #schtasks.exe /ST requires the HH:mm format, so the minute must be zero-padded (issue 4950) - _st_minute="$(printf "%02d" "$_randomminute")" - #SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_st_minute" '/RU' "$_myname" '/RP' "$_password" '/TR' "$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'" >/dev/null - echo SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_st_minute" '/RU' "$_myname" '/RP' "$_password" '/TR' "\"$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'\"" | cmd.exe >/dev/null + #SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_randomminute" '/RU' "$_myname" '/RP' "$_password" '/TR' "$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'" >/dev/null + echo SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_randomminute" '/RU' "$_myname" '/RP' "$_password" '/TR' "\"$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'\"" | cmd.exe >/dev/null echo } @@ -7096,7 +6078,7 @@ installcronjob() { fi _t=$(_time) random_minute=$(_math $_t % 60) - random_hour=$(_math $_t / 60 % 6) + random_hour=$(_math $_t / 60 % 24) if ! _exists "$_CRONTAB" && _exists "fcrontab"; then _CRONTAB="fcrontab" @@ -7105,7 +6087,7 @@ installcronjob() { if ! _exists "$_CRONTAB"; then if _exists cygpath && _exists schtasks.exe; then _info "It seems you are on Windows, let's install the Windows scheduler task." - if _install_win_taskscheduler "$lesh" "$_c_entry" "$random_minute" "$random_hour"; then + if _install_win_taskscheduler "$lesh" "$_c_entry" "$random_minute"; then _info "Successfully installed Windows scheduler task." return 0 else @@ -7119,39 +6101,15 @@ installcronjob() { return 1 fi _info "Installing cron job" - _cron_entry="$random_minute $random_hour,$(_math "$random_hour" + 6),$(_math "$random_hour" + 12),$(_math "$random_hour" + 18) * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" - _cron_entries="$($_CRONTAB -l 2>/dev/null)" - if [ "$?" != "0" ]; then - #when the user has no crontab yet, crontab -l also exits non-zero; - #only that case may proceed with an empty list. Any other listing - #failure must abort: piping an incomplete list back into 'crontab -' - #would wipe the user's existing cron jobs (issue 3079) - _cron_list_err="$($_CRONTAB -l 2>&1 >/dev/null)" - #separate greps: BRE alternation \| is a GNU extension and Solaris - #grep takes only a single -e pattern - if echo "$_cron_list_err" | grep -i "no crontab" >/dev/null || - echo "$_cron_list_err" | grep -i "no fcrontab" >/dev/null || - echo "$_cron_list_err" | grep -i "can't open" >/dev/null; then - _cron_entries="" - else - _err "Can not list the current cron jobs: $_cron_list_err" - _err "Refusing to install the cron job, that could wipe your existing cron jobs." - _err "Please add this cron job manually:" - _err "$_cron_entry" - return 1 - fi - fi - if ! echo "$_cron_entries" | grep "$PROJECT_ENTRY --cron"; then + if ! $_CRONTAB -l | grep "$PROJECT_ENTRY --cron"; then if _exists uname && uname -a | grep SunOS >/dev/null; then _CRONTAB_STDIN="$_CRONTAB --" else _CRONTAB_STDIN="$_CRONTAB -" fi - { - if [ "$_cron_entries" ]; then - echo "$_cron_entries" - fi - echo "$_cron_entry" + $_CRONTAB -l | { + cat + echo "$random_minute $random_hour * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" } | $_CRONTAB_STDIN fi if [ "$?" != "0" ]; then @@ -7347,7 +6305,7 @@ _deactivate() { _err "Cannot get new order for domain." return 1 fi - _authorizations_seg="$(echo "$response" | _json_decode | _authorizations_from_order)" + _authorizations_seg="$(echo "$response" | _egrep_o '"authorizations" *: *\[[^\]*\]' | cut -d '[' -f 2 | tr -d ']' | tr -d '"')" _debug2 _authorizations_seg "$_authorizations_seg" if [ -z "$_authorizations_seg" ]; then _err "_authorizations_seg not found." @@ -7379,8 +6337,7 @@ _deactivate() { fi _debug "Trigger validation." vtype="$(_getIdType "$_d_domain")" - # Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018 - entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^{]*"type":"'$vtype'"[^}]*')" + entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$entry" ]; then _err "$d: Cannot get domain token" @@ -7464,62 +6421,6 @@ deactivate() { done } -#reads the output of "openssl x509 -text" from stdin, prints the hex AKI -#the value is on the line right after the extension header; "grep -A" is not -#portable (Solaris /usr/bin/grep: "illegal option -- A"), so select from the -#header to EOF and keep the second line of that range -_extractAKI() { - sed -n '/X509v3 Authority Key Identifier/,$p' | _head_n 2 | _tail_n 1 | tr -d ': ' | sed "s/keyid//" -} - -#cert -_getAKI() { - _cert="$1" - ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | _extractAKI -} - -#cert -_getSerial() { - _cert="$1" - ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -serial -noout | cut -d = -f 2 -} - -#cert -#Compute the ARI/replaces certID for a cert: base64url(AKI).base64url(Serial) -#per RFC 9773 Section 4.1. -_getARICertID() { - _cert="$1" - _aki=$(_getAKI "$_cert") - _ser=$(_getSerial "$_cert") - _debug2 "_aki" "$_aki" - _debug2 "_ser" "$_ser" - - # RFC 9773 Section 4.1 requires the DER-encoded INTEGER value bytes of - # serialNumber. When the high bit of the first byte is set (>= 0x80) DER - # prepends a 0x00 sign byte to keep the integer positive; openssl's hex - # output strips that, so add it back. Boulder (LE) accepts either form, - # but Sectigo (ZeroSSL) is strict and rejects newOrder with HTTP 401 - # "replaces field does not identify a certificate" if the byte is missing. - case "$_ser" in - [89aAbBcCdDeEfF]*) _ser="00$_ser" ;; - esac - - _akiurl="$(echo "$_aki" | _h2b | _base64 | _url_replace)" - _debug2 "_akiurl" "$_akiurl" - _serurl="$(echo "$_ser" | _h2b | _base64 | _url_replace)" - _debug2 "_serurl" "$_serurl" - - printf "%s.%s" "$_akiurl" "$_serurl" -} - -#cert -_get_ARI() { - _cert="$1" - _ari_certID="$(_getARICertID "$_cert")" - _ARI_URL="$ACME_RENEWAL_INFO/$_ari_certID" - _get "$_ARI_URL" -} - # Detect profile file if not specified as environment variable _detect_profile() { if [ -n "$PROFILE" -a -f "$PROFILE" ]; then @@ -7568,7 +6469,6 @@ _initconf() { #NO_TIMESTAMP=1 " >"$ACCOUNT_CONF_PATH" - chmod 600 "$ACCOUNT_CONF_PATH" fi } @@ -7604,10 +6504,10 @@ _precheck() { return 1 fi - if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then - _info "It is recommended to install socat or python first." - _info "We use socat or python for the standalone server, which is used for standalone mode." - _info "If you don't want to use standalone mode, you may ignore this warning." + if ! _exists "socat"; then + _err "It is recommended to install socat first." + _err "We use socat for the standalone server, which is used for standalone mode." + _err "If you don't want to use standalone mode, you may ignore this warning." fi return 0 @@ -7631,15 +6531,6 @@ _installalias() { _c_home="$1" _initpath - _alias_bin="$LE_WORKING_DIR/$PROJECT_ENTRY" - if [ ! -f "$_alias_bin" ]; then - #ACME_PACKAGED install: no copy in LE_WORKING_DIR, alias the current script - _script="$(_readlink "$_SCRIPT_")" - if [ -f "$_script" ]; then - _alias_bin="$_script" - fi - fi - _envfile="$LE_WORKING_DIR/$PROJECT_ENTRY.env" if [ "$_upgrading" ] && [ "$_upgrading" = "1" ]; then echo "$(cat "$_envfile")" | sed "s|^LE_WORKING_DIR.*$||" >"$_envfile" @@ -7657,11 +6548,7 @@ _installalias() { else _sed_i "/^export LE_CONFIG_HOME/d" "$_envfile" fi - _setopt "$_envfile" "alias $PROJECT_ENTRY" "=" "\"$_alias_bin$_c_entry\"" - if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion" ]; then - #the completion file does nothing when sourced by a non-bash shell - _setopt "$_envfile" ". \"$LE_WORKING_DIR/$PROJECT_ENTRY.completion\"" - fi + _setopt "$_envfile" "alias $PROJECT_ENTRY" "=" "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\"" _profile="$(_detect_profile)" if [ "$_profile" ]; then @@ -7684,7 +6571,7 @@ _installalias() { else _sed_i "/^setenv LE_CONFIG_HOME/d" "$_cshfile" fi - _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$_alias_bin$_c_entry\"" + _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\"" _setopt "$_csh_profile" "source \"$_cshfile\"" fi @@ -7696,7 +6583,7 @@ _installalias() { if [ "$_c_home" ]; then _setopt "$_cshfile" "setenv LE_CONFIG_HOME" " " "\"$LE_CONFIG_HOME\"" fi - _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$_alias_bin$_c_entry\"" + _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\"" _setopt "$_tcsh_profile" "source \"$_cshfile\"" fi @@ -7770,38 +6657,25 @@ install() { chmod 700 "$LE_CONFIG_HOME" fi - if [ "$ACME_PACKAGED" ]; then - #the script and its hooks are managed by a system package manager, - #do not copy them into LE_WORKING_DIR. https://github.com/acmesh-official/acme.sh/issues/7135 - _info "ACME_PACKAGED is set, skipping the script copy." - else - cp "$PROJECT_ENTRY" "$LE_WORKING_DIR/" && chmod +x "$LE_WORKING_DIR/$PROJECT_ENTRY" + cp "$PROJECT_ENTRY" "$LE_WORKING_DIR/" && chmod +x "$LE_WORKING_DIR/$PROJECT_ENTRY" - if [ "$?" != "0" ]; then - _err "Installation failed, cannot copy $PROJECT_ENTRY" - return 1 - fi - - _info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY" - - if [ -f "$PROJECT_ENTRY.completion" ]; then - cp "$PROJECT_ENTRY.completion" "$LE_WORKING_DIR/" - _debug "Installed bash completion to $LE_WORKING_DIR/$PROJECT_ENTRY.completion" - fi + if [ "$?" != "0" ]; then + _err "Installation failed, cannot copy $PROJECT_ENTRY" + return 1 fi + _info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY" + if [ "$_ACME_IN_CRON" != "1" ] && [ -z "$_noprofile" ]; then _installalias "$_c_home" fi - if [ -z "$ACME_PACKAGED" ]; then - for subf in $_SUB_FOLDERS; do - if [ -d "$subf" ]; then - mkdir -p "$LE_WORKING_DIR/$subf" - cp "$subf"/* "$LE_WORKING_DIR"/"$subf"/ - fi - done - fi + for subf in $_SUB_FOLDERS; do + if [ -d "$subf" ]; then + mkdir -p "$LE_WORKING_DIR/$subf" + cp "$subf"/* "$LE_WORKING_DIR"/"$subf"/ + fi + done if [ ! -f "$ACCOUNT_CONF_PATH" ]; then _initconf @@ -7813,12 +6687,6 @@ install() { if [ "$_DEFAULT_CERT_HOME" != "$CERT_HOME" ]; then _saveaccountconf "CERT_HOME" "$CERT_HOME" - # Create the custom cert home now instead of on first issuance, so the - # user can see --install honored it. - # https://github.com/acmesh-official/acme.sh/issues/4756 - if [ ! -d "$CERT_HOME" ]; then - mkdir -p "$CERT_HOME" - fi fi if [ "$_DEFAULT_ACCOUNT_KEY_PATH" != "$ACCOUNT_KEY_PATH" ]; then @@ -7829,7 +6697,7 @@ install() { installcronjob "$_c_home" fi - if [ -z "$NO_DETECT_SH" ] && [ -z "$ACME_PACKAGED" ]; then + if [ -z "$NO_DETECT_SH" ]; then #Modify shebang if _exists bash; then _bash_path="$(bash -c "command -v bash 2>/dev/null")" @@ -7854,9 +6722,7 @@ install() { if [ "$_accountemail" ]; then _saveaccountconf "ACCOUNT_EMAIL" "$_accountemail" fi - if [ -z "$ACME_PACKAGED" ]; then - _saveaccountconf "UPGRADE_HASH" "$(_getUpgradeHash)" - fi + _saveaccountconf "UPGRADE_HASH" "$(_getUpgradeHash)" _info OK } @@ -7870,12 +6736,7 @@ uninstall() { _uninstallalias - if [ -z "$ACME_PACKAGED" ]; then - #don't remove the script when it is managed by a system package manager, - #LE_WORKING_DIR may point to the packaged files - rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY" - rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion" - fi + rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY" _info "The keys and certs are in \"$(__green "$LE_CONFIG_HOME")\". You can remove them by yourself." } @@ -7911,26 +6772,21 @@ cron() { _initpath _info "$(__green "===Starting cron===")" if [ "$AUTO_UPGRADE" = "1" ]; then - if [ "$ACME_PACKAGED" ]; then - _info "ACME_PACKAGED is set, skipping the auto upgrade." - else - export LE_WORKING_DIR - ( - if ! upgrade; then - _err "Cron: Upgrade failed!" - return 1 - fi - ) - . "$LE_WORKING_DIR/$PROJECT_ENTRY" >/dev/null - - if [ -t 1 ]; then - __INTERACTIVE="1" + export LE_WORKING_DIR + ( + if ! upgrade; then + _err "Cron: Upgrade failed!" + return 1 fi + ) + . "$LE_WORKING_DIR/$PROJECT_ENTRY" >/dev/null - _info "Automatically upgraded to: $VER" + if [ -t 1 ]; then + __INTERACTIVE="1" fi + + _info "Automatically upgraded to: $VER" fi - _TREAT_SKIP_AS_SUCCESS="1" renewAll _ret="$?" _ACME_IN_CRON="" @@ -7977,14 +6833,6 @@ _send_notify() { continue fi if ! ( - # The dns/deploy hooks export _H1.._H5 in the main process, so the - # values are inherited here. Clear them: a stale Authorization header - # from another service must not leak into the notify request. - export _H1="" - export _H2="" - export _H3="" - export _H4="" - export _H5="" if ! . "$_n_hook_file"; then _err "Error loading file $_n_hook_file. Please check your API file and try again." return 1 @@ -8102,11 +6950,8 @@ Commands: -ccr, --create-csr Create CSR, professional use. --create-domain-key Create an domain private key, professional use. --update-account Update account info. - --update-account-key Rotate account key. --register-account Register account key. --deactivate-account Deactivate the account. - --make-dns-persist-value Print the DNS TXT record(s) to enable persistent DNS validation - (draft-ietf-acme-dns-persist-01). Use with -d . --create-account-key Create an account private key, professional use. --install-cronjob Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job. --uninstall-cronjob Uninstall the cron job. The 'uninstall' command can do this automatically. @@ -8131,9 +6976,6 @@ Parameters: If no match, the default offered chain will be used. (default: empty) See: $_PREFERRED_CHAIN_WIKI - --cert-profile, --certificate-profile If the CA offers profiles, select the desired profile - See: $_PROFILESELECTION_WIKI - --valid-to Request the NotAfter field of the cert. See: $_VALIDITY_WIKI --valid-from Request the NotBefore field of the cert. @@ -8154,34 +6996,16 @@ Parameters: --dns [dns_hook] Use dns manual mode or dns api. Defaults to manual mode when argument is omitted. See: $_DNS_API_WIKI - --dns-persist Use dns-persist-01 validation (draft-ietf-acme-dns-persist-01). - Requires the persistent _validation-persist TXT record to already - exist. Use '--make-dns-persist-value' to print the value to add. - --dnssleep The time in seconds to wait for all the txt records to propagate in dns api mode. It's not necessary to use this by default, $PROJECT_NAME polls dns status by DOH automatically. - -k, --keylength Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256 (default), ec-384, ec-521. - -ak, --accountkeylength Specifies the account key length: 2048, 3072, 4096, 8192 or ec-256 (default), ec-384, ec-521. + -k, --keylength Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384, ec-521. + -ak, --accountkeylength Specifies the account key length: 2048, 3072, 4096 --log [file] Specifies the log file. Defaults to \"$DEFAULT_LOG_FILE\" if argument is omitted. --log-level <1|2> Specifies the log level, default is $DEFAULT_LOG_LEVEL. --syslog <0|3|6|7> Syslog level, 0: disable syslog, 3: error, 6: info, 7: debug. --eab-kid Key Identifier for External Account Binding. --eab-hmac-key HMAC key for External Account Binding. - --dns-persist-wildcard Used with '--make-dns-persist-value'. Adds 'policy=wildcard' to the - generated TXT record so the issuer is also authorized for wildcards - and subdomains (draft-ietf-acme-dns-persist-01). It is implied when - the domain given to -d is a wildcard (e.g. '*.example.com'); the - record itself is always published at the base domain. - --dns-persist-ca-name Used with '--make-dns-persist-value'. Use the given CA identity domain - (e.g. 'ssl.com') as the issuer-domain-name in the TXT record. If - omitted, the identities are read from the ACME directory's - 'caaIdentities' field and one record is printed per identity. - --dns-persist-days Used with '--make-dns-persist-value'. Add a 'persistUntil' field to - the TXT record so the record self-expires N days from now (the CA - will refuse new validations against the record after that time). - If omitted, the record has no expiry. - These parameters are to install the cert to nginx/Apache or any other server after issue/renew a cert: @@ -8200,17 +7024,13 @@ Parameters: --config-home Specifies the home dir to save all the configurations. --useragent Specifies the user agent string. it will be saved for future use too. -m, --email Specifies the account email, only valid for the '--install' and '--update-account' command. - Multiple emails can be given as a comma-separated list: 'a@example.com,b@example.com' --accountkey Specifies the account key path, only valid for the '--install' command. --days Specifies the days to renew the cert when using '--issue' command. The default value is $DEFAULT_RENEW days. - A negative value renews that many days before the cert expiry. - Negative values could be used to specify a number of days relative to the expiration date of the certificate. --httpport Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer. --tlsport Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer. --local-address Specifies the standalone/tls server listening address, in case you have multiple ip addresses. --listraw Only used for '--list' command, list the certs in raw format. -se, --stop-renew-on-error Only valid for '--renew-all' command. Stop if one cert has error in renewal. - --treat-skip-as-success Only valid for '--renew-all' command. Treat skipped certs as success, return 0 instead of $RENEW_SKIP. --insecure Do not check the server certificate, in some devices, the api server's certificate may not be trusted. --ca-bundle Specifies the path to the CA certificate bundle to verify api server's certificate. --ca-path Specifies directory containing CA certificates in PEM format, used by wget or curl. @@ -8230,11 +7050,8 @@ Parameters: --ocsp, --ocsp-must-staple Generate OCSP-Must-Staple extension. --always-force-new-domain-key Generate new domain key on renewal. Otherwise, the domain key is not changed by default. --auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. Defaults to 1 if argument is omitted. - --listen-v4 Force standalone/tls server to listen at ipv4 only. - By default the standalone server listens on both ipv4 and ipv6. - --listen-v6 Force standalone/tls server to listen at ipv6 only. - --request-v4 Force client requests to use ipv4 to connect to the CA server. - --request-v6 Force client requests to use ipv6 to connect to the CA server. + --listen-v4 Force standalone/tls server to listen at ipv4. + --listen-v6 Force standalone/tls server to listen at ipv6. --openssl-bin Specifies a custom openssl bin location. --use-wget Force to use wget, if you have both curl and wget installed. --yes-I-know-dns-manual-mode-enough-go-ahead-please Force use of dns manual mode. @@ -8254,17 +7071,13 @@ Parameters: --revoke-reason <0-10> The reason for revocation, can be used in conjunction with the '--revoke' command. See: $_REVOKE_WIKI - --password Add a password to the exported pfx or pkcs8 file. Use with '--to-pkcs12' or '--to-pkcs8'. + --password Add a password to exported pfx file. Use with --to-pkcs12. " } installOnline() { - if [ "$ACME_PACKAGED" ]; then - _err "ACME_PACKAGED is set: acme.sh is managed by the system package manager, please use it to upgrade." - return 1 - fi _info "Installing from online archive." _branch="$BRANCH" @@ -8288,9 +7101,7 @@ installOnline() { cd "$PROJECT_NAME-$_branch" chmod +x $PROJECT_ENTRY - ./$PROJECT_ENTRY --install "$@" - _install_rc="$?" - if [ "$_install_rc" = "0" ]; then + if ./$PROJECT_ENTRY --install "$@"; then _info "Install success!" fi @@ -8298,9 +7109,6 @@ installOnline() { rm -rf "$PROJECT_NAME-$_branch" rm -f "$localname" - # Propagate the install result so a failed upgrade is not reported as - # success. https://github.com/acmesh-official/acme.sh/issues/6477 - exit "$_install_rc" ) } @@ -8322,10 +7130,6 @@ _getUpgradeHash() { } upgrade() { - if [ "$ACME_PACKAGED" ]; then - _err "ACME_PACKAGED is set: acme.sh is managed by the system package manager, please use it to upgrade." - exit 1 - fi if ( _initpath [ -z "$FORCE" ] && [ "$(_getUpgradeHash)" = "$(_readaccountconf "UPGRADE_HASH")" ] && _info "Already up to date!" && exit 0 @@ -8366,24 +7170,6 @@ _processAccountConf() { _saveaccountconf "ACME_USE_WGET" "$ACME_USE_WGET" fi - if [ "$_request_v6" ]; then - _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$_request_v6" - _clearaccountconf "ACME_USE_IPV4_REQUESTS" - ACME_USE_IPV4_REQUESTS= - elif [ "$_request_v4" ]; then - _saveaccountconf "ACME_USE_IPV4_REQUESTS" "$_request_v4" - _clearaccountconf "ACME_USE_IPV6_REQUESTS" - ACME_USE_IPV6_REQUESTS= - elif [ "$ACME_USE_IPV6_REQUESTS" ]; then - _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$ACME_USE_IPV6_REQUESTS" - _clearaccountconf "ACME_USE_IPV4_REQUESTS" - ACME_USE_IPV4_REQUESTS= - elif [ "$ACME_USE_IPV4_REQUESTS" ]; then - _saveaccountconf "ACME_USE_IPV4_REQUESTS" "$ACME_USE_IPV4_REQUESTS" - _clearaccountconf "ACME_USE_IPV6_REQUESTS" - ACME_USE_IPV6_REQUESTS= - fi - } _checkSudo() { @@ -8397,16 +7183,9 @@ _checkSudo() { return 0 fi if [ -n "$SUDO_COMMAND" ]; then - #The SUDO_* env vars are often inherited into shells that were not - #started as `sudo acme.sh` at all (e.g. `sudo su - user`, or - #`sudo pct enter ` on Proxmox, which copies them into the - #container). Only warn when sudo was used to run acme.sh itself; - #anything else means the sudo happened further up and is fine. - #https://github.com/acmesh-official/acme.sh/issues/6400 - if _contains "$SUDO_COMMAND" "$PROJECT_ENTRY"; then - return 1 - fi - return 0 + #it's a normal user doing "sudo su", or `sudo -i` or `sudo -s`, or `sudo su acmeuser1` + _endswith "$SUDO_COMMAND" /bin/su || _contains "$SUDO_COMMAND" "/bin/su " || grep "^$SUDO_COMMAND\$" /etc/shells >/dev/null 2>&1 + return $? fi #otherwise return 1 @@ -8556,8 +7335,6 @@ _process() { _local_address="" _log_level="" _auto_upgrade="" - _request_v4="" - _request_v6="" _listen_v4="" _listen_v6="" _openssl_bin="" @@ -8574,11 +7351,6 @@ _process() { _preferred_chain="" _valid_from="" _valid_to="" - _certificate_profile="" - _extended_key_usage="" - _dns_persist_wildcard="" - _dns_persist_ca_name="" - _dns_persist_days="" while [ ${#} -gt 0 ]; do case "${1}" in @@ -8667,29 +7439,12 @@ _process() { --update-account | --updateaccount) _CMD="updateaccount" ;; - --update-account-key | --updateaccountkey) - _CMD="updateaccountkey" - ;; --register-account | --registeraccount) _CMD="registeraccount" ;; --deactivate-account) _CMD="deactivateaccount" ;; - --make-dns-persist-value | --makednspersistvalue) - _CMD="makednspersistvalue" - ;; - --dns-persist-wildcard | --dnspersistwildcard) - _dns_persist_wildcard="1" - ;; - --dns-persist-ca-name | --dnspersistcaname) - _dns_persist_ca_name="$2" - shift - ;; - --dns-persist-days | --dnspersistdays) - _dns_persist_days="$2" - shift - ;; --set-notify) _CMD="setnotify" ;; @@ -8699,9 +7454,6 @@ _process() { --set-default-chain) _CMD="setdefaultchain" ;; - --list-profiles) - _CMD="list_profiles" - ;; -d | --domain) _dvalue="$2" @@ -8732,9 +7484,6 @@ _process() { -f | --force) FORCE="1" ;; - --treat-skip-as-success | --treatskipassuccess) - _TREAT_SKIP_AS_SUCCESS="1" - ;; --staging | --test) STAGE="1" ;; @@ -8833,14 +7582,6 @@ _process() { _webroot="$_webroot,$wvalue" fi ;; - --dns-persist) - wvalue="$W_DNS_PERSIST" - if [ -z "$_webroot" ]; then - _webroot="$wvalue" - else - _webroot="$_webroot,$wvalue" - fi - ;; --dnssleep) _dnssleep="$2" Le_DNSSleep="$_dnssleep" @@ -8928,10 +7669,6 @@ _process() { _valid_to="$2" shift ;; - --certificate-profile | --cert-profile) - _certificate_profile="$2" - shift - ;; --httpport) _httpport="$2" Le_HTTPPort="$_httpport" @@ -9002,7 +7739,7 @@ _process() { shift ;; --extended-key-usage) - _extended_key_usage="$2" + Le_ExtKeyUse="$2" shift ;; --ocsp-must-staple | --ocsp) @@ -9055,18 +7792,6 @@ _process() { fi AUTO_UPGRADE="$_auto_upgrade" ;; - --request-v4) - _request_v4="1" - ACME_USE_IPV4_REQUESTS="1" - _request_v6="" - ACME_USE_IPV6_REQUESTS="" - ;; - --request-v6) - _request_v6="1" - ACME_USE_IPV6_REQUESTS="1" - _request_v4="" - ACME_USE_IPV4_REQUESTS="" - ;; --listen-v4) _listen_v4="1" Le_Listen_V4="$_listen_v4" @@ -9207,22 +7932,6 @@ _process() { _debug2 LE_WORKING_DIR "$LE_WORKING_DIR" - # --valid-to pins the cert lifetime, so a creation-anchored (positive) - # --days schedule can not apply and is rejected. A negative --days is - # anchored to the expiry and composes with a relative --valid-to: the - # cert renews that many days before the expiry. - if [ "$_days" ] && [ "$_valid_to" ]; then - if ! _startswith "$_valid_to" "+"; then - _err "--days can not be used together with a fixed-date --valid-to: such a cert can not be renewed automatically." - return 1 - fi - if ! _startswith "$_days" "-"; then - _err "A positive --days can not be used together with --valid-to, the renewal time is derived from the expiry time." - _err "Use a negative --days to renew that many days before the expiry, or omit --days to renew 1 day before the expiry." - return 1 - fi - fi - if [ "$DEBUG" ]; then version if [ "$_server" ]; then @@ -9235,13 +7944,13 @@ _process() { uninstall) uninstall "$_nocron" ;; upgrade) upgrade ;; issue) - issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage" + issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" ;; deploy) deploy "$_domain" "$_deploy_hook" "$_ecc" ;; signcsr) - signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage" + signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" ;; showcsr) showcsr "$_csr" "$_domain" @@ -9270,15 +7979,9 @@ _process() { updateaccount) updateaccount ;; - updateaccountkey) - updateaccountkey "$_accountkeylength" - ;; deactivateaccount) deactivateaccount ;; - makednspersistvalue) - makednspersistvalue "$_domain" "$_dns_persist_wildcard" "$_dns_persist_ca_name" "$_dns_persist_days" - ;; list) list "$_listraw" "$_domain" ;; @@ -9292,7 +7995,7 @@ _process() { toPkcs "$_domain" "$_password" "$_ecc" ;; toPkcs8) - toPkcs8 "$_domain" "$_password" "$_ecc" + toPkcs8 "$_domain" "$_ecc" ;; createAccountKey) createAccountKey "$_accountkeylength" @@ -9301,7 +8004,7 @@ _process() { createDomainKey "$_domain" "$_keylength" ;; createCSR) - createCSR "$_domain" "$_altdomains" "$_ecc" "$_extended_key_usage" + createCSR "$_domain" "$_altdomains" "$_ecc" ;; setnotify) setnotify "$_notify_hook" "$_notify_level" "$_notify_mode" "$_notify_source" @@ -9312,9 +8015,6 @@ _process() { setdefaultchain) setdefaultchain "$_preferred_chain" ;; - list_profiles) - list_profiles - ;; *) if [ "$_CMD" ]; then _err "Invalid command: $_CMD" diff --git a/acme.sh.completion b/acme.sh.completion deleted file mode 100644 index 26cb88da..00000000 --- a/acme.sh.completion +++ /dev/null @@ -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 </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 </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 ".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 diff --git a/deploy/ali_cdn.sh b/deploy/ali_cdn.sh index 3c28674e..70a2e532 100644 --- a/deploy/ali_cdn.sh +++ b/deploy/ali_cdn.sh @@ -83,6 +83,6 @@ _set_cdn_domain_ssl_certificate_query() { query=$query'&SignatureMethod=HMAC-SHA1' query=$query"&SignatureNonce=$(_ali_nonce)" query=$query'&SignatureVersion=1.0' - query=$query'&Timestamp='$(_ali_timestamp) + query=$query'&Timestamp='$(_timestamp) query=$query'&Version=2018-05-10' } diff --git a/deploy/ali_dcdn.sh b/deploy/ali_dcdn.sh index 27d3a726..14ac500a 100644 --- a/deploy/ali_dcdn.sh +++ b/deploy/ali_dcdn.sh @@ -83,6 +83,6 @@ _set_dcdn_domain_ssl_certificate_query() { query=$query'&SignatureMethod=HMAC-SHA1' query=$query"&SignatureNonce=$(_ali_nonce)" query=$query'&SignatureVersion=1.0' - query=$query'&Timestamp='$(_ali_timestamp) + query=$query'&Timestamp='$(_timestamp) query=$query'&Version=2018-01-15' } diff --git a/deploy/baidu_cdn.sh b/deploy/baidu_cdn.sh deleted file mode 100644 index 7fe31f9b..00000000 --- a/deploy/baidu_cdn.sh +++ /dev/null @@ -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 -} diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh deleted file mode 100644 index 394b431f..00000000 --- a/deploy/byteplus_alb.sh +++ /dev/null @@ -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 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 -} diff --git a/deploy/cachefly.sh b/deploy/cachefly.sh deleted file mode 100644 index 7841b20b..00000000 --- a/deploy/cachefly.sh +++ /dev/null @@ -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 -} diff --git a/deploy/cpanel_uapi.sh b/deploy/cpanel_uapi.sh index 02ef6b3e..e5381b61 100644 --- a/deploy/cpanel_uapi.sh +++ b/deploy/cpanel_uapi.sh @@ -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 diff --git a/deploy/directadmin.sh b/deploy/directadmin.sh deleted file mode 100644 index 3f60a088..00000000 --- a/deploy/directadmin.sh +++ /dev/null @@ -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 -} diff --git a/deploy/docker.sh b/deploy/docker.sh index 276172aa..264963ae 100755 --- a/deploy/docker.sh +++ b/deploy/docker.sh @@ -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" @@ -73,18 +71,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 @@ -126,20 +112,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 @@ -217,22 +189,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 diff --git a/deploy/edgio.sh b/deploy/edgio.sh deleted file mode 100644 index 1acd0c8f..00000000 --- a/deploy/edgio.sh +++ /dev/null @@ -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 -} diff --git a/deploy/fortigate.sh b/deploy/fortigate.sh deleted file mode 100644 index f00ca1cb..00000000 --- a/deploy/fortigate.sh +++ /dev/null @@ -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 <" - - return 0 -} diff --git a/deploy/kemplm.sh b/deploy/kemplm.sh deleted file mode 100755 index 4cdfcbbe..00000000 --- a/deploy/kemplm.sh +++ /dev/null @@ -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 -} diff --git a/deploy/keyhelp.sh b/deploy/keyhelp.sh deleted file mode 100644 index f66d27ce..00000000 --- a/deploy/keyhelp.sh +++ /dev/null @@ -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>/{//!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>/{//!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 -} diff --git a/deploy/keyhelp_api.sh b/deploy/keyhelp_api.sh deleted file mode 100644 index 75e9d951..00000000 --- a/deploy/keyhelp_api.sh +++ /dev/null @@ -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 -} diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh deleted file mode 100644 index 9a1a0fcf..00000000 --- a/deploy/localcopy.sh +++ /dev/null @@ -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 -} diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh deleted file mode 100644 index 4a8c9dc9..00000000 --- a/deploy/multideploy.sh +++ /dev/null @@ -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 "" "?" -_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 "" -_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 "" -_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_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 -_deploy_service() { - _name="$1" - _hook="$2" - - _debug2 "SERVICE" "$_name" - _debug2 "HOOK" "$_hook" - - _info "$(__green "Deploying") to '$_name' using '$_hook'" - _deploy "$_cdomain" "$_hook" -} diff --git a/deploy/mydevil.sh b/deploy/mydevil.sh index 8954f822..bd9868aa 100755 --- a/deploy/mydevil.sh +++ b/deploy/mydevil.sh @@ -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 } diff --git a/deploy/netlify.sh b/deploy/netlify.sh deleted file mode 100644 index 8d25f74c..00000000 --- a/deploy/netlify.sh +++ /dev/null @@ -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 -} diff --git a/deploy/panos.sh b/deploy/panos.sh index fcfd6fb5..0dc1b2f0 100644 --- a/deploy/panos.sh +++ b/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>.*/\2/g') - _commit_job_id=$job_id - elif [ "$type" = 'job_status' ]; then - job_status=$(echo "$1" | tr -d '\n' | sed 's/^.*\([^<]*\)<\/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/.*\(\|\|\)\([^<]*\).*/\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="excludedexcluded" - #content="type=commit&action=partial&key=$_panos_key&cmd=$_exclude_scope$_panos_user" + #_exclude_scope="excludeexcludeexclude" + #content="type=commit&action=partial&key=$_panos_key&cmd=$_exclude_scopeacmekeytest" 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" "$_panos_user" | _url_encode) + cmd=$(printf "%s" "$_panos_user" | _url_encode) else - cmd=$(printf "%s" "$_panos_user" | _url_encode) + _exclude_scope="excludeexclude" + cmd=$(printf "%s" "$_exclude_scope$_panos_user" | _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" "$_commit_job_id" | _url_encode) - content="type=op&key=$_panos_key&cmd=$cmd" - fi - - # Push changes - if [ "$type" = 'push' ]; then - echo "**** Pushing changes ****" - cmd=$(printf "%s" "$_panos_template_stack$_panos_user" | _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 } diff --git a/deploy/proxmoxbs.sh b/deploy/proxmoxbs.sh index 30599a44..d1146454 100644 --- a/deploy/proxmoxbs.sh +++ b/deploy/proxmoxbs.sh @@ -115,25 +115,6 @@ HEREDOC _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 + _post "$_json_payload" "$_target_url" "" POST "application/json" } diff --git a/deploy/proxmoxve.sh b/deploy/proxmoxve.sh index fd8d69d8..f9de590c 100644 --- a/deploy/proxmoxve.sh +++ b/deploy/proxmoxve.sh @@ -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" } diff --git a/deploy/qiniu.sh b/deploy/qiniu.sh index 3737ed4e..02250ed3 100644 --- a/deploy/qiniu.sh +++ b/deploy/qiniu.sh @@ -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")" diff --git a/deploy/routeros.sh b/deploy/routeros.sh index 328fabbd..ef9c6954 100644 --- a/deploy/routeros.sh +++ b/deploy/routeros.sh @@ -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,7 +143,6 @@ 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=\\\"\\\";\ diff --git a/deploy/shelly.sh b/deploy/shelly.sh deleted file mode 100644 index dbdab346..00000000 --- a/deploy/shelly.sh +++ /dev/null @@ -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 -# 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 -} diff --git a/deploy/ssh.sh b/deploy/ssh.sh index 0bf3ee48..c66e2e19 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -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 diff --git a/deploy/strongswan.sh b/deploy/strongswan.sh index 80353c54..14567d17 100644 --- a/deploy/strongswan.sh +++ b/deploy/strongswan.sh @@ -33,7 +33,7 @@ strongswan_deploy() { return 1 fi _info _confdir "${_confdir}" - __deploy_cert "stroke" "${_confdir}" "$@" + __deploy_cert "$@" "stroke" "${_confdir}" ${_ipsec} reload fi # For modern vici mode @@ -50,7 +50,7 @@ strongswan_deploy() { _err "no swanctl config dir is found" return 1 fi - __deploy_cert "vici" "${_confdir}" "$@" + __deploy_cert "$@" "vici" "${_confdir}" ${_swanctl} --load-creds fi if [ -z "${_swanctl}" ] && [ -z "${_ipsec}" ]; then @@ -63,13 +63,13 @@ strongswan_deploy() { #################### Private functions below ################################## __deploy_cert() { - _swan_mode="${1}" - _confdir="${2}" - _cdomain="${3}" - _ckey="${4}" - _ccert="${5}" - _cca="${6}" - _cfullchain="${7}" + _cdomain="${1}" + _ckey="${2}" + _ccert="${3}" + _cca="${4}" + _cfullchain="${5}" + _swan_mode="${6}" + _confdir="${7}" _debug _cdomain "${_cdomain}" _debug _ckey "${_ckey}" _debug _ccert "${_ccert}" diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index 336980a5..3bfc9b02 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -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 @@ -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." @@ -322,8 +322,8 @@ synology_dsm_deploy() { _savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN" _savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME" else - _savedeployconf SYNO_USERNAME "$SYNO_USERNAME" "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" } @@ -428,6 +424,11 @@ _temp_admin_cleanup() { fi } +#_cleardeployconf key +_cleardeployconf() { + _cleardomainconf "SAVED_$1" +} + # key _check2cleardeployconfexp() { _key="$1" diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh index 33e3dfa0..940cde2e 100644 --- a/deploy/truenas_ws.sh +++ b/deploy/truenas_ws.sh @@ -16,12 +16,7 @@ # # # API KEY # # Use the folowing URL to create a new API token: /ui/apikeys -# export DEPLOY_TRUENAS_APIKEY="" -# Optional: -# export DEPLOY_TRUENAS_HOSTNAME="" -# export DEPLOY_TRUENAS_PROTOCOL="wss" # ws or wss -# export DEPLOY_TRUENAS_PORT="443" # optional, e.g. 80, 443, 8443 - +# export DEPLOY_TRUENAS_APIKEY="/dev/null 2>&1 # fail quietly if we're not running as root diff --git a/deploy/unifios.sh b/deploy/unifios.sh deleted file mode 100644 index 82b65c69..00000000 --- a/deploy/unifios.sh +++ /dev/null @@ -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 -} diff --git a/deploy/windows_rdp.sh b/deploy/windows_rdp.sh deleted file mode 100644 index e708e9a7..00000000 --- a/deploy/windows_rdp.sh +++ /dev/null @@ -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 <&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 "${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//\n/g' | sed 's/]*>//g' | tr -d ' ' | grep -i "$label" | sed "s/$label<\/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' -} diff --git a/dnsapi/dns_1984hosting.sh b/dnsapi/dns_1984hosting.sh index 8ed9b8ef..906ea443 100755 --- a/dnsapi/dns_1984hosting.sh +++ b/dnsapi/dns_1984hosting.sh @@ -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" diff --git a/dnsapi/dns_acmedns.sh b/dnsapi/dns_acmedns.sh index a21f8ef0..f3f50233 100755 --- a/dnsapi/dns_acmedns.sh +++ b/dnsapi/dns_acmedns.sh @@ -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 diff --git a/dnsapi/dns_active24.sh b/dnsapi/dns_active24.sh index 0f24c53a..c56dd363 100755 --- a/dnsapi/dns_active24.sh +++ b/dnsapi/dns_active24.sh @@ -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,15 +70,21 @@ 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=1 + p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) _debug "h" "$h" @@ -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" +} diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh index b8ca9169..53a82f91 100755 --- a/dnsapi/dns_ali.sh +++ b/dnsapi/dns_ali.sh @@ -18,9 +18,7 @@ Ali_DNS_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 @@ -35,7 +33,7 @@ dns_ali_add() { } dns_ali_rm() { - fulldomain=$(_idn "$1") + fulldomain=$1 txtvalue=$2 Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}" Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}" @@ -71,8 +69,8 @@ _ali_rest() { 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) + signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _url_encode upper-hex)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64) + signature=$(printf "%s" "$signature" | _url_encode upper-hex) url="$endpoint?Signature=$signature" if [ "$mtd" = "GET" ]; then @@ -98,28 +96,13 @@ _ali_rest() { fi } -# 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 + #_head_n 1 "$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 -} diff --git a/dnsapi/dns_autodns.sh b/dnsapi/dns_autodns.sh index e26d699b..ce566978 100644 --- a/dnsapi/dns_autodns.sh +++ b/dnsapi/dns_autodns.sh @@ -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" -} - _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 " %s %s %s - " "$_autodns_user_xml" "$_autodns_password_xml" "$_autodns_context_xml" + " "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT" } # Arguments: diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh index 1face1c8..c88c9d9c 100755 --- a/dnsapi/dns_aws.sh +++ b/dnsapi/dns_aws.sh @@ -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" @@ -162,7 +161,7 @@ _get_root() { h=$(printf "%s" "$domain" | cut -d . -f "$i"-100 | sed 's/\./\\./g') _debug "Checking domain: $h" if [ -z "$h" ]; then - _err "invalid domain" + _error "invalid domain" return 1 fi diff --git a/dnsapi/dns_azure.sh b/dnsapi/dns_azure.sh index f9d84706..03feaf63 100644 --- a/dnsapi/dns_azure.sh +++ b/dnsapi/dns_azure.sh @@ -340,17 +340,8 @@ _azure_getaccess_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")" + 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 \") diff --git a/dnsapi/dns_baidu.sh b/dnsapi/dns_baidu.sh deleted file mode 100644 index dfad8eeb..00000000 --- a/dnsapi/dns_baidu.sh +++ /dev/null @@ -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 < +Author: ARNik arnik@arnik.ru ' Beget_Api="https://api.beget.com/api" diff --git a/dnsapi/dns_bh.sh b/dnsapi/dns_bh.sh deleted file mode 100755 index fbb69ef2..00000000 --- a/dnsapi/dns_bh.sh +++ /dev/null @@ -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 -} diff --git a/dnsapi/dns_bhosted.sh b/dnsapi/dns_bhosted.sh deleted file mode 100644 index 46ddd5dd..00000000 --- a/dnsapi/dns_bhosted.sh +++ /dev/null @@ -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 ...." - _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. 12345 -_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}>\\([^<]*\\).*:\\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" ""; 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: 12345 - _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" -} diff --git a/dnsapi/dns_bookmyname.sh b/dnsapi/dns_bookmyname.sh index cf3f1e3e..668cf074 100644 --- a/dnsapi/dns_bookmyname.sh +++ b/dnsapi/dns_bookmyname.sh @@ -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 ##################### diff --git a/dnsapi/dns_calrissia.sh b/dnsapi/dns_calrissia.sh deleted file mode 100644 index 01ca3092..00000000 --- a/dnsapi/dns_calrissia.sh +++ /dev/null @@ -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 -} diff --git a/dnsapi/dns_cdmon.sh b/dnsapi/dns_cdmon.sh deleted file mode 100644 index 470fb5fe..00000000 --- a/dnsapi/dns_cdmon.sh +++ /dev/null @@ -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 -} diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh index 7b383c43..736742f3 100755 --- a/dnsapi/dns_cf.sh +++ b/dnsapi/dns_cf.sh @@ -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 diff --git a/dnsapi/dns_cloudns.sh b/dnsapi/dns_cloudns.sh index 2c543271..8bb0e00d 100755 --- a/dnsapi/dns_cloudns.sh +++ b/dnsapi/dns_cloudns.sh @@ -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 @@ -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")" diff --git a/dnsapi/dns_cn.sh b/dnsapi/dns_cn.sh index e06a2be6..79698e88 100644 --- a/dnsapi/dns_cn.sh +++ b/dnsapi/dns_cn.sh @@ -15,8 +15,7 @@ CN_API="https://beta.api.core-networks.de" ######## Public functions ##################### dns_cn_add() { - # Core-Networks API requires punycode for IDN domains - fulldomain=$(_idn "$1") + fulldomain=$1 txtvalue=$2 if ! _cn_login; then @@ -59,8 +58,7 @@ dns_cn_add() { } dns_cn_rm() { - # Core-Networks API requires punycode for IDN domains - fulldomain=$(_idn "$1") + fulldomain=$1 txtvalue=$2 if ! _cn_login; then diff --git a/dnsapi/dns_comlaude.sh b/dnsapi/dns_comlaude.sh deleted file mode 100644 index 2aa2dba9..00000000 --- a/dnsapi/dns_comlaude.sh +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env sh - -# shellcheck disable=SC2034 -dns_comlaude_info='comlaude.com -Site: comlaude.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_comlaude -Options: - COMLAUDE_USERNAME User account - COMLAUDE_PASSWORD User password - COMLAUDE_API_KEY generated API key - COMLAUDE_GROUP_ID Group ID in comlaude user profile - Get it from the https://www.comlaude.com -Issues: github.com/acmesh-official/acme.sh/issues/7112 -' -# ===== CONFIG ===== -COMLAUDE_API="https://api.comlaude.com" - -########## AUTH ########## - -_comlaude_auth() { - _debug "Checking cached ComLaude token" - - # Try to get token from account.conf - if [ -z "$COMLAUDE_ACCESS_TOKEN" ]; then - COMLAUDE_ACCESS_TOKEN="$(_readaccountconf_mutable COMLAUDE_ACCESS_TOKEN)" - COMLAUDE_TOKEN_EXPIRY="$(_readaccountconf_mutable COMLAUDE_TOKEN_EXPIRY)" - fi - - _now=$(_time) - if [ -n "$COMLAUDE_ACCESS_TOKEN" ] && [ -n "$COMLAUDE_TOKEN_EXPIRY" ] && [ "$_now" -lt "$COMLAUDE_TOKEN_EXPIRY" ]; then - _debug "Using cached ComLaude token (valid ${COMLAUDE_TOKEN_EXPIRY} > ${_now})" - return 0 - fi - - _info "ComLaude auth..." - _comlaude_body="{\"username\":\"$COMLAUDE_USERNAME\",\"password\":\"$COMLAUDE_PASSWORD\",\"api_key\":\"$COMLAUDE_API_KEY\"}" - _comlaude_response="$(_post "$_comlaude_body" "$COMLAUDE_API/api_login" "" "POST" "application/json")" - - if ! _contains "$_comlaude_response" "access_token"; then - _err "Auth failed: $_comlaude_response" - return 1 - fi - - COMLAUDE_ACCESS_TOKEN=$(echo "$_comlaude_response" | _egrep_o '"access_token":"[^"]*"' | cut -d'"' -f4) - # store expiracy from api reply l'API ("expires_in" in seconds) - _comlaude_expires_in=$(echo "$_comlaude_response" | _egrep_o '"expires_in":[0-9]*' | cut -d: -f2) - [ -z "$_comlaude_expires_in" ] && _comlaude_expires_in=3000 # fallback if no info - - COMLAUDE_TOKEN_EXPIRY=$(($(_time) + _comlaude_expires_in - 60)) # margin of 60s to secure renew - - _saveaccountconf_mutable COMLAUDE_ACCESS_TOKEN "$COMLAUDE_ACCESS_TOKEN" - _saveaccountconf_mutable COMLAUDE_TOKEN_EXPIRY "$COMLAUDE_TOKEN_EXPIRY" - - return 0 -} - -########## DOMAIN RESOLUTION ########## - -_comlaude_get_root() { - COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}" - if [ -z "$COMLAUDE_GROUP_ID" ]; then - _err "Missing COMLAUDE_GROUP_ID" - return 1 - fi - - _comlaude_input_domain="$1" - _comlaude_input_domain="${_comlaude_input_domain#_acme-challenge.}" - case "$_comlaude_input_domain" in - \*.*) _comlaude_input_domain="${_comlaude_input_domain#*.}" ;; - esac - - _debug "Normalized domain: $_comlaude_input_domain" - - _comlaude_i=1 - while true; do - _comlaude_d=$(printf "%s" "$_comlaude_input_domain" | cut -d . -f "$_comlaude_i-") - [ -z "$_comlaude_d" ] && { - _debug "No matching domain found for $_comlaude_input_domain" - return 1 - } - - # don't test unnecessary levels - # registered domain : TLD only (no dot after cut). - case "$_comlaude_d" in - *.*) : ;; - *) - _debug "Skipping bare TLD candidate: $_comlaude_d" - _comlaude_i=$((_comlaude_i + 1)) - continue - ;; - esac - - _debug "Checking domain: $_comlaude_d" - - _comlaude_retry=0 - _comlaude_max_retry=3 # to avoid network errors - _comlaude_DOM_ID="" - _comlaude_Z_ID="" - - while [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ]; do - export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" - _debug "Full URL: $COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone" - _comlaude_response="$(_get "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone")" - _H1="" - - _debug "RAW response for $_comlaude_d (try $((_comlaude_retry + 1))): $_comlaude_response" - - # If empty -> true network issue, we retry - if [ -z "$_comlaude_response" ]; then - _comlaude_retry=$((_comlaude_retry + 1)) - [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2 - continue - fi - - # 404 -> domain not found in that level. no retry : continue - if echo "$_comlaude_response" | grep -q '"status_code":404'; then - _debug "404 for $_comlaude_d, moving to next level (not retrying)" - break - fi - - # Domain missing (200 reply, data empty) -> continue - if echo "$_comlaude_response" | grep -q '"data":\[\]'; then - _debug "Empty data for $_comlaude_d, moving to next level" - break - fi - - # Extraction via _egrep_o - _comlaude_DOM_ID="$(echo "$_comlaude_response" | _egrep_o '"id":"[^"]*"' | head -n1 | cut -d':' -f2 | tr -d '"')" - _comlaude_Z_ID="$(echo "$_comlaude_response" | _egrep_o '"active_zone":\{"id":"[^"]*"' | _egrep_o '"id":"[^"]*"$' | cut -d':' -f2 | tr -d '"')" - - if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then - break - fi - - # 200 reply but malformed data / noid -> retry transport - _comlaude_retry=$((_comlaude_retry + 1)) - [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2 - done - - _debug "_comlaude_DOM_ID=$_comlaude_DOM_ID" - _debug "_comlaude_Z_ID=$_comlaude_Z_ID" - - if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then - _comlaude_domain="$_comlaude_d" - _comlaude_domain_id="$_comlaude_DOM_ID" - _comlaude_zone_id="$_comlaude_Z_ID" - return 0 - fi - - _comlaude_i=$((_comlaude_i + 1)) - done -} -########## ADD TXT ########## - -dns_comlaude_add() { - fulldomain="$1" - txtvalue="$2" - - COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}" - COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}" - COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}" - COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}" - - if [ -z "$COMLAUDE_USERNAME" ] || [ -z "$COMLAUDE_PASSWORD" ] || [ -z "$COMLAUDE_API_KEY" ]; then - _err "You didn't specify ComLaude credentials (COMLAUDE_USERNAME, COMLAUDE_PASSWORD, COMLAUDE_API_KEY)." - return 1 - fi - - # Backup variable after validation - _saveaccountconf_mutable COMLAUDE_USERNAME "$COMLAUDE_USERNAME" - _saveaccountconf_mutable COMLAUDE_PASSWORD "$COMLAUDE_PASSWORD" - _saveaccountconf_mutable COMLAUDE_API_KEY "$COMLAUDE_API_KEY" - _saveaccountconf_mutable COMLAUDE_GROUP_ID "$COMLAUDE_GROUP_ID" - - _info "Adding TXT: $fulldomain" - _comlaude_auth || return 1 - _comlaude_get_root "$fulldomain" || return 1 - - _debug "Root: $_comlaude_domain" - - _comlaude_data="{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"value\":\"$txtvalue\",\"ttl\":60}" - - export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" - export _H2="Content-Type: application/json" - - _comlaude_response="$(_post "$_comlaude_data" "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records")" - - _H1="" - _H2="" - if ! echo "$_comlaude_response" | grep -q '"id"'; then - _err "Failed to create TXT" - _debug "$_comlaude_response" - return 1 - fi - - return 0 -} - -########## REMOVE TXT ########## - -dns_comlaude_rm() { - fulldomain="$1" - txtvalue="$2" - - COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}" - COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}" - COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}" - COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}" - - _info "Removing TXT: $fulldomain" - - _comlaude_auth || return 1 - _comlaude_get_root "$fulldomain" || return 1 - - export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" - _comlaude_encoded_name="$(printf '%s' "$fulldomain" | _url_encode)" - _comlaude_encoded_value="$(printf '%s' "$txtvalue" | _url_encode)" - _comlaude_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records?filter[type]=TXT&filter[name]=$_comlaude_encoded_name&filter[value]=$_comlaude_encoded_value" - _comlaude_response="$(_get "$_comlaude_url")" - _H1="" - - _debug "Filtered records response: $_comlaude_response" - - # first "id" top-level of reply (record itself, - # always on first position of each data[] object) - _comlaude_record_id="$(echo "$_comlaude_response" | _egrep_o '"data":\[\{"id":"[^"]*"' | _egrep_o '"[^"]*"$' | tr -d '"')" - - if [ -z "$_comlaude_record_id" ]; then - _info "No matching TXT record found to delete for $fulldomain / $txtvalue" - return 0 - fi - - _debug "Deleting record $_comlaude_record_id" - - export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" - _comlaude_del_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records/$_comlaude_record_id" - _comlaude_del_resp="$(_post "" "$_comlaude_del_url" "" "DELETE")" - _H1="" - - if echo "$_comlaude_del_resp" | grep -q '"error"'; then - _err "Delete failed for $_comlaude_record_id" - _debug "$_comlaude_del_resp" - return 1 - fi - - _info "Deleted record $_comlaude_record_id" - return 0 -} diff --git a/dnsapi/dns_constellix.sh b/dnsapi/dns_constellix.sh index 7251f8b2..6a50e199 100644 --- a/dnsapi/dns_constellix.sh +++ b/dnsapi/dns_constellix.sh @@ -117,7 +117,7 @@ dns_constellix_rm() { #################### Private functions below ################################## _get_root() { - domain=$(echo "$1" | _lower_case) + domain=$1 i=2 p=1 _debug "Detecting root zone" @@ -156,9 +156,6 @@ _constellix_rest() { data="$3" _debug "$ep" - # Prevent rate limit - _sleep 2 - rdate=$(date +"%s")"000" hmac=$(printf "%s" "$rdate" | _hmac sha1 "$(printf "%s" "$CONSTELLIX_Secret" | _hex_dump | tr -d ' ')" | _base64) diff --git a/dnsapi/dns_cpanel.sh b/dnsapi/dns_cpanel.sh index 6939c3f1..a6991403 100755 --- a/dnsapi/dns_cpanel.sh +++ b/dnsapi/dns_cpanel.sh @@ -38,7 +38,7 @@ dns_cpanel_add() { fi # adding entry _info "Adding the entry" - stripped_fulldomain="${fulldomain%."$_domain"}" + stripped_fulldomain=$(echo "$fulldomain" | sed "s/.$_domain//") _debug "Adding $stripped_fulldomain to $_domain zone" _myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=add_zone_record&domain=$_domain&name=$stripped_fulldomain&type=TXT&txtdata=$txtvalue&ttl=1" if _successful_update; then return 0; fi @@ -128,27 +128,13 @@ _get_root() { _err "Primary domain list not found!" return 1 fi - # Pick the LONGEST matching zone, dot-anchored: with both domain.tld and - # sub.domain.tld zones on the account, cPanel stores the record in the - # most specific zone, so add and rm must both resolve to that one. - _domain="" - for d in $_domains; do - _debug "Checking if $fulldomain ends with $d" - # case with quoted patterns gives an exact literal suffix match; - # _endswith treats the needle as a regex, so its dots would let - # xdomain.tld wrongly match zone domain.tld - case "$fulldomain" in - "$d" | *".$d") - if [ "${#d}" -gt "${#_domain}" ]; then - _domain="$d" - fi - ;; - esac + for _domain in $_domains; do + _debug "Checking if $fulldomain ends with $_domain" + if (_endswith "$fulldomain" "$_domain"); then + _debug "Root domain: $_domain" + return 0 + fi done - if [ -n "$_domain" ]; then - _debug "Root domain: $_domain" - return 0 - fi return 1 } diff --git a/dnsapi/dns_cpanel_uapi.sh b/dnsapi/dns_cpanel_uapi.sh deleted file mode 100755 index 02a777ae..00000000 --- a/dnsapi/dns_cpanel_uapi.sh +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_cpanel_uapi_info='cPanel UAPI - Manage DNS via cPanel UAPI. Works with API tokens and Two-Factor Authentication. -Site: cpanel.net -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_cpanel_uapi -Options: - cPanel_Username Username - cPanel_Apitoken API Token - cPanel_Hostname Server URL. E.g. "https://hostname:port" - cPanel_TTL optional TXT record TTL in seconds. Default: 120 -Issues: github.com/acmesh-official/acme.sh/issues/6877 -Author: Adam Bodnar -' - -######## Public functions ##################### - -# Used to add txt record -dns_cpanel_uapi_add() { - fulldomain=$1 - txtvalue=$2 - - _info "Adding TXT record via cPanel UAPI" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - if ! _cpanel_uapi_get_root; then - _err "No matching root domain for $fulldomain found" - return 1 - fi - - # Build the record name relative to the zone - _escaped_domain=$(echo "$_domain" | sed 's/\./\\./g') - _record_name=$(echo "$fulldomain" | sed "s/\.${_escaped_domain}$//") - _debug "Record name: $_record_name in zone $_domain" - - # Get the current SOA serial (required by mass_edit_zone) - if ! _cpanel_uapi_get_serial "$_domain"; then - _err "Failed to get zone serial for $_domain" - return 1 - fi - _debug "Zone serial: $_serial" - - # Use configurable TTL, default 120 seconds - _ttl="${cPanel_TTL:-$(_readaccountconf_mutable cPanel_TTL)}" - case "$_ttl" in - "") - _ttl=120 - ;; - *[!0-9]*) - _debug "Invalid cPanel_TTL provided, falling back to default 120" - _ttl=120 - ;; - esac - - # Build JSON and URL-encode it for the add parameter - _add_json=$(printf '{"dname":"%s","ttl":%s,"record_type":"TXT","data":["%s"]}' "$_record_name" "$_ttl" "$txtvalue") - _debug "add_json: $_add_json" - _add_json_encoded=$(printf '%s' "$_add_json" | _url_encode) - _debug "add_json (encoded): $_add_json_encoded" - - if ! _cpanel_uapi_request "execute/DNS/mass_edit_zone?zone=${_domain}&serial=${_serial}&add=${_add_json_encoded}"; then - _err "Request to add TXT record failed for zone $_domain" - return 1 - fi - _debug "_result: $_result" - - if _contains "$_result" '"status":1'; then - _info "TXT record added successfully" - return 0 - fi - _err "Failed to add TXT record." - _err "Response: $_result" - return 1 -} - -# Used to remove the txt record after validation -dns_cpanel_uapi_rm() { - fulldomain=$1 - txtvalue=$2 - - _info "Removing TXT record via cPanel UAPI" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - if ! _cpanel_uapi_get_root; then - _err "No matching root domain for $fulldomain found" - return 1 - fi - - if ! _cpanel_uapi_findentry; then - _info "Entry doesn't exist, nothing to delete" - return 0 - fi - - _debug "Deleting record with line_index=$_line_index" - if ! _cpanel_uapi_get_serial "$_domain"; then - _err "Failed to get zone serial for $_domain" - return 1 - fi - if ! _cpanel_uapi_request "execute/DNS/mass_edit_zone?zone=${_domain}&serial=${_serial}&remove=${_line_index}"; then - _err "Request to remove TXT record failed for zone $_domain" - return 1 - fi - _debug "_result: $_result" - - if _contains "$_result" '"status":1'; then - _info "TXT record removed successfully" - return 0 - fi - _err "Failed to remove TXT record." - _err "Response: $_result" - return 1 -} - -#################### Private functions below ################################## - -_cpanel_uapi_checkcredentials() { - cPanel_Username="${cPanel_Username:-$(_readaccountconf_mutable cPanel_Username)}" - cPanel_Apitoken="${cPanel_Apitoken:-$(_readaccountconf_mutable cPanel_Apitoken)}" - cPanel_Hostname="${cPanel_Hostname:-$(_readaccountconf_mutable cPanel_Hostname)}" - - if [ -z "$cPanel_Username" ] || [ -z "$cPanel_Apitoken" ] || [ -z "$cPanel_Hostname" ]; then - cPanel_Username="" - cPanel_Apitoken="" - cPanel_Hostname="" - _err "You haven't specified cPanel_Username, cPanel_Apitoken, and cPanel_Hostname." - return 1 - fi - - # Remove trailing slash from hostname if present - cPanel_Hostname=$(echo "$cPanel_Hostname" | sed 's|/$||') - - _saveaccountconf_mutable cPanel_Username "$cPanel_Username" - _saveaccountconf_mutable cPanel_Apitoken "$cPanel_Apitoken" - _saveaccountconf_mutable cPanel_Hostname "$cPanel_Hostname" - - if [ -n "$cPanel_TTL" ]; then - case "$cPanel_TTL" in - *[!0-9]*) - _info "Ignoring invalid cPanel_TTL: $cPanel_TTL" - cPanel_TTL="" - ;; - *) - _saveaccountconf_mutable cPanel_TTL "$cPanel_TTL" - ;; - esac - fi - return 0 -} - -_cpanel_uapi_request() { - export _H1="Authorization: cpanel $cPanel_Username:$cPanel_Apitoken" - _result=$(_get "$cPanel_Hostname/$1") - return $? -} - -_cpanel_uapi_get_root() { - if ! _cpanel_uapi_checkcredentials; then return 1; fi - - if ! _cpanel_uapi_request "execute/DomainInfo/list_domains"; then - _err "Request to cPanel API failed while listing domains" - return 1 - fi - _debug "DomainInfo response length: ${#_result}" - - if ! _contains "$_result" '"status":1'; then - _err "cPanel UAPI request failed. Is the API token correct?" - _debug "Response: $_result" - return 1 - fi - - # Extract main_domain - _main_domain=$(echo "$_result" | _egrep_o '"main_domain":"[^"]*"' | _head_n 1 | sed 's/.*"main_domain":"//;s/"//') - _debug "main_domain: $_main_domain" - - # Extract addon_domains (array of strings) - _addon_domains=$(echo "$_result" | _egrep_o '"addon_domains":\[[^]]*\]' | sed 's/.*"addon_domains":\[//;s/\]$//' | _egrep_o '"[a-zA-Z0-9._-]+"' | sed 's/"//g') - _debug "addon_domains: $_addon_domains" - - # Build list of all domains to check - _all_domains="$_main_domain $_addon_domains" - _debug "All domains: $_all_domains" - - # Find the matching root domain (prefer longest match) - _best_match="" - _best_len=0 - for _check_domain in $_all_domains; do - if [ -z "$_check_domain" ]; then continue; fi - if _endswith "$fulldomain" "$_check_domain"; then - _len=${#_check_domain} - if [ "$_len" -gt "$_best_len" ]; then - _best_match="$_check_domain" - _best_len="$_len" - fi - fi - done - - if [ -n "$_best_match" ]; then - _domain="$_best_match" - _debug "Root domain: $_domain" - return 0 - fi - return 1 -} - -_cpanel_uapi_get_serial() { - _zone="$1" - if ! _cpanel_uapi_request "execute/DNS/parse_zone?zone=${_zone}"; then - _err "Request to parse zone failed for $_zone" - return 1 - fi - - # Split JSON records onto separate lines using a POSIX-portable sed literal newline - # (\\n in sed replacement is a GNU/BusyBox extension; a backslash-newline works everywhere) - _soa_line=$(echo "$_result" | sed 's/},{/},\ -{/g' | grep '"record_type":"SOA"' | _head_n 1) - _debug "SOA line: $_soa_line" - - if [ -z "$_soa_line" ]; then - _err "SOA record not found for zone $_zone" - _debug "parse_zone response: $_result" - return 1 - fi - - # Extract the third element from data_b64 array (serial is index 2, 0-based) - # data_b64 format: ["ns","admin","SERIAL","refresh","retry","expire","minimum"] - _serial_b64=$(echo "$_soa_line" | _egrep_o '"data_b64":\[[^]]*\]' | sed 's/"data_b64":\[//;s/\]//' | sed 's/"//g' | cut -d',' -f3) - _debug "serial_b64: $_serial_b64" - - if [ -z "$_serial_b64" ]; then - _err "Could not extract serial from SOA record" - return 1 - fi - - _serial=$(printf '%s' "$_serial_b64" | _dbase64) - _debug "Decoded serial: $_serial" - - if [ -z "$_serial" ]; then - _err "Failed to decode serial" - return 1 - fi - return 0 -} - -_cpanel_uapi_findentry() { - _debug "Finding TXT entry for $fulldomain with value $txtvalue" - - if ! _cpanel_uapi_request "execute/DNS/parse_zone?zone=${_domain}"; then - _err "Request to parse zone failed for $_domain" - return 1 - fi - _debug "parse_zone result length: ${#_result}" - - # Base64-encode the txtvalue to match against data_b64 in the response - _b64_txtvalue=$(printf '%s' "$txtvalue" | _base64) - _debug "b64_txtvalue: $_b64_txtvalue" - - # Split records onto separate lines, find matching TXT record by base64 value - _line_index=$(echo "$_result" | sed 's/},{/},\ -{/g' | grep '"record_type":"TXT"' | grep -F "$_b64_txtvalue" | _egrep_o '"line_index":[0-9]+' | _head_n 1 | cut -d: -f2) - _debug "line_index: $_line_index" - - if [ -n "$_line_index" ]; then - _debug "Entry found with line_index=$_line_index" - return 0 - fi - return 1 -} diff --git a/dnsapi/dns_creoline.sh b/dnsapi/dns_creoline.sh deleted file mode 100644 index f4d76f8e..00000000 --- a/dnsapi/dns_creoline.sh +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_creoline_info='creoline -Site: https://www.creoline.com/de -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_creoline -Help: https://help.creoline.com -Options: - creolineApiToken - creolineApiSecret -Issues: github.com/acmesh-official/acme.sh/issues/7103 -' - -creolineApi="https://api.creoline.com/v1" - -######## Public functions ##################### - -# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPB8" -dns_creoline_add() { - fulldomain=$1 - txtvalue=$2 - - creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}" - creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}" - - if [ -z "$creolineApiToken" ] || [ -z "$creolineApiSecret" ]; then - _err "Error required creoline API Token or creoline API Secret not specified." - _err "Please set it with the Command 'export creolineApiToken=' and 'export creolineApiSecret='." - return 1 - else - _saveaccountconf_mutable creolineApiToken "$creolineApiToken" - _saveaccountconf_mutable creolineApiSecret "$creolineApiSecret" - fi - - _debug "Detecting the root dns zone." - if ! _get_root "$fulldomain"; then - _err "Error on detecting the root dns zone." - return 1 - fi - - _info "Adding record" - if _creoline_rest POST "dns/zone/$_domain/record" "{\"type\":\"TXT\",\"host\":\"$_sub_domain\",\"record\":\"$txtvalue\",\"ttl\":\"60\"}"; then - if _contains "$response" "$txtvalue"; then - _info "Added, OK" - return 0 - else - _err "Add txt record error." - return 1 - fi - fi - _err "Add txt record error." - return 1 -} - -#fulldomain txtvalue -dns_creoline_rm() { - fulldomain=$1 - txtvalue=$2 - - creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}" - creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}" - - _debug "Detecting the root dns zone." - if ! _get_root "$fulldomain"; then - _err "Error on detecting the root dns zone." - return 1 - fi - - _info "Getting earlier created txt record." - if ! _creoline_rest GET "dns/zone/$_domain/record/type/TXT/record/$txtvalue"; then - if _contains "$response" "errors" || _contains "$response" "message"; then - _err "Error on getting earlier created txt record." - return 1 - fi - _err "Error on getting earlier created txt record." - return 1 - fi - - record_id=$(echo "$response" | _egrep_o "\"id\"[ ]*:[ ]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") - _debug "record_id" "$record_id" - - if [ -z "$record_id" ]; then - _err "Error on deleting earlier created txt record. No record id found in response." - return 1 - fi - - _info "Deleting earlier created txt record." - if ! _creoline_rest DELETE "dns/zone/$_domain/record/$record_id"; then - if _contains "$response" "errors" || _contains "$response" "message"; then - _err "Error on deleting earlier created txt record." - return 1 - fi - _err "Error on deleting earlier created txt record." - return 1 - fi - - _info "Deleted, OK" - return 0 -} - -#################### Private functions below ################################## -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com -_get_root() { - domain=$1 - if ! _creoline_rest GET "dns/zone/root/$domain"; then - return 1 - fi - - _sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") - _debug _sub_domain "$_sub_domain" - - _domain=$(echo "$response" | _egrep_o "\"domain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") - _debug _domain "$_domain" - - if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then - return 1 - fi -} - -_creoline_rest() { - method=$1 - uri="$2" - data="$3" - timestamp=$(_time) - canonical_request="${timestamp}.${creolineApi}/${uri}" - signature_hash=$(printf "%s" "$canonical_request" | _hmac sha256 "$(printf "%s" "$creolineApiSecret" | _hex_dump | tr -d " ")" hex) - - _debug method "$method" - _debug uri "$uri" - _debug data "$data" - - _debug2 timestamp "$timestamp" - _debug2 canonical_request "$canonical_request" - _debug2 signature_hash "$signature_hash" - - token_trimmed=$(echo "$creolineApiToken" | tr -d '"') - hmac_trimmed=$(echo "$signature_hash" | tr -d '"') - - export _H1="Content-Type: application/json" - - if [ "$token_trimmed" ]; then - export _H2="X-Api-Token: $token_trimmed" - fi - - if [ "$hmac_trimmed" ]; then - export _H3="X-Creoline-Api-Signature: $hmac_trimmed" - fi - - if [ "$timestamp" ]; then - export _H4="X-Creoline-Api-Timestamp: $timestamp" - fi - - if [ "$method" != "GET" ]; then - response="$(_post "$data" "$creolineApi/$uri" "" "$method")" - else - response="$(_get "$creolineApi/$uri")" - fi - - if [ "$?" != "0" ]; then - _err "error $uri" - return 1 - fi - - _debug response "$response" - - if _contains "$response" "errors"; then - error=$(echo "$response" | _egrep_o "\"errors\":[[]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | tr -d "[") - _err "Error: $error" - _err "URI:$uri" - return 1 - elif _contains "$response" "message"; then - message=$(echo "$response" | _egrep_o "\"message\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \") - _err "Error: $message" - _err "URI:$uri" - return 1 - fi - - return 0 -} diff --git a/dnsapi/dns_curanet.sh b/dnsapi/dns_curanet.sh index 0ef03fea..f57afa1f 100644 --- a/dnsapi/dns_curanet.sh +++ b/dnsapi/dns_curanet.sh @@ -15,7 +15,7 @@ CURANET_REST_URL="https://api.curanet.dk/dns/v1/Domains" CURANET_AUTH_URL="https://apiauth.dk.team.blue/auth/realms/Curanet/protocol/openid-connect/token" CURANET_ACCESS_TOKEN="" -######## Public functions #################### +######## Public functions ##################### #Usage: dns_curanet_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_curanet_add() { @@ -154,7 +154,7 @@ _get_root() { export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN" response="$(_get "$CURANET_REST_URL/$h/Records" "" "")" - if [ ! "$(echo "$response" | _egrep_o "Entity not found|Bad Request")" ]; then + if [ ! "$(echo "$response" | _egrep_o "Entity not found")" ]; then _domain=$h return 0 fi diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh index 6677b32f..a585e772 100644 --- a/dnsapi/dns_cyon.sh +++ b/dnsapi/dns_cyon.sh @@ -101,8 +101,6 @@ _cyon_load_parameters() { # This header is required for curl calls. _H1="X-Requested-With: XMLHttpRequest" export _H1 - _H3="User-Agent: cyon-dns-acmesh/1.0" - export _H3 } _cyon_print_header() { @@ -127,11 +125,7 @@ _cyon_print_header() { } _cyon_get_cookie_header() { - # Extract all cookies from the response headers (case-insensitive) - _cookies="$(grep -i "^set-cookie:" "$HTTP_HEADER" | sed 's/^[Ss]et-[Cc]ookie: //' | sed 's/;.*//' | tr '\n' '; ' | sed 's/; $//')" - if [ -n "$_cookies" ]; then - printf "Cookie: %s" "$_cookies" - fi + printf "Cookie: %s" "$(grep "cyon=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'cyon=[^;]*;' | tr -d ';')" } _cyon_login() { @@ -161,12 +155,7 @@ _cyon_login() { _get "https://my.cyon.ch/" >/dev/null - # Update cookie after loading main page (only if new cookies are set) - _new_cookies="$(_cyon_get_cookie_header)" - if [ -n "$_new_cookies" ]; then - _H2="$_new_cookies" - export _H2 - fi + # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request. # 2FA authentication with OTP? if [ -n "${CY_OTP_Secret}" ]; then @@ -195,13 +184,6 @@ _cyon_login() { fi _info " success" - - # Update cookie after 2FA (only if new cookies are set) - _new_cookies="$(_cyon_get_cookie_header)" - if [ -n "$_new_cookies" ]; then - _H2="$_new_cookies" - export _H2 - fi fi _info "" @@ -223,17 +205,7 @@ _cyon_change_domain_env() { domain_env="$(printf "%s" "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')" _debug "Changing domain environment to ${domain_env}" - domain_page_response="$(_get "https://my.cyon.ch/domain/")" - _debug domain_page_response "${domain_page_response}" - - # Check if we got an error response (JSON) instead of HTML - if printf "%s" "${domain_page_response}" | grep -q '"iserror":true'; then - _err " $(printf "%s" "${domain_page_response}" | _cyon_get_response_message)" - _err "" - return 1 - fi - - gloo_item_key="$(printf "%s" "${domain_page_response}" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")" + gloo_item_key="$(_get "https://my.cyon.ch/domain/" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")" _debug gloo_item_key "${gloo_item_key}" domain_env_url="https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/${gloo_item_key}" @@ -285,15 +257,15 @@ _cyon_delete_txt() { list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async" - list_txt_response="$(_get "${list_txt_url}")" + list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')" _debug list_txt_response "${list_txt_response}" if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi # Find and delete all acme challenge entries for the $fulldomain. - _dns_entries="$(printf "%s\n" "${list_txt_response}" | _egrep_o 'data-hash=\\"[^"]*\\" data-identifier=\\"[^"]*\\"' | sed 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\"/\1 \2/')" + _dns_entries="$(printf "%b\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')" - printf "%s\n" "${_dns_entries}" | while read -r _hash _identifier; do + printf "%s" "${_dns_entries}" | while read -r _hash _identifier; do dns_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)" dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)" @@ -332,11 +304,11 @@ _cyon_get_response_message() { } _cyon_get_response_status() { - _egrep_o '"status":[a-zA-Z0-9]*' | cut -d : -f 2 + _egrep_o '"status":[a-zA-z0-9]*' | cut -d : -f 2 } _cyon_get_validation_status() { - _egrep_o '"valid":[a-zA-Z0-9]*' | cut -d : -f 2 + _egrep_o '"valid":[a-zA-z0-9]*' | cut -d : -f 2 } _cyon_get_response_success() { @@ -344,7 +316,7 @@ _cyon_get_response_success() { } _cyon_get_environment_change_status() { - _egrep_o '"authenticated":[a-zA-Z0-9]*' | cut -d : -f 2 + _egrep_o '"authenticated":[a-zA-z0-9]*' | cut -d : -f 2 } _cyon_check_if_2fa_missed() { diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh deleted file mode 100644 index 6ad60442..00000000 --- a/dnsapi/dns_czechia.sh +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env sh - -# dns_czechia.sh - CZECHIA.COM/ZONER DNS API for acme.sh (DNS-01) -# -# Documentation: https://api.czechia.com/swagger/index.html - -#shellcheck disable=SC2034 -dns_czechia_info='[ - {"name":"CZ_AuthorizationToken","usage":"Your API token from CZECHIA.COM/Zoner administration.","required":"1"}, - {"name":"CZ_Zones","usage":"Managed zones separated by comma or space (e.g. \"example.com\").","required":"1"}, - {"name":"CZ_API_BASE","usage":"Defaults to https://api.czechia.com","required":"0"} -]' - -dns_czechia_add() { - fulldomain="$1" - txtvalue="$2" - - _debug "dns_czechia_add fulldomain='$fulldomain'" - - if [ -z "$fulldomain" ] || [ -z "$txtvalue" ]; then - _err "dns_czechia_add: missing fulldomain or txtvalue" - return 1 - fi - - _czechia_load_conf || return 1 - - _current_zone=$(_czechia_pick_zone "$fulldomain") - if [ -z "$_current_zone" ]; then - _err "No matching zone found for $fulldomain. Please check CZ_Zones." - return 1 - fi - - _czechia_tab="$(printf '\t')" - _cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//") - _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//") - - if [ -z "$_cz" ] || [ -z "$_tk" ]; then - _err "Missing zone or CZ_AuthorizationToken." - return 1 - fi - - _url="$CZ_API_BASE/api/DNS/$_cz/TXT" - _fd=$(printf "%s" "$fulldomain" | _lower_case | sed 's/\.$//') - - if [ "$_fd" = "$_cz" ]; then - _h="@" - else - # Remove the literal "." suffix from _fd, if present - _h=${_fd%."$_cz"} - [ "$_h" = "$_fd" ] && _h="@" - fi - [ -z "$_h" ] && _h="@" - - _info "Adding TXT record for $_h in zone $_cz" - - _h_esc=$(printf "%s" "$_h" | sed 's/\\/\\\\/g; s/"/\\"/g') - _txt_esc=$(printf "%s" "$txtvalue" | sed 's/\\/\\\\/g; s/"/\\"/g') - _body="{\"hostName\":\"$_h_esc\",\"text\":\"$_txt_esc\",\"ttl\":300,\"publishZone\":1}" - - _debug "URL: $_url" - _debug "Body: $_body" - - export _H1="Content-Type: application/json" - export _H2="AuthorizationToken: $_tk" - - _res="$(_post "$_body" "$_url" "" "POST")" - _post_exit="$?" - _debug2 "Response: $_res" - - if [ "$_post_exit" -ne 0 ]; then - _err "API request failed. exit code $_post_exit" - return 1 - fi - - if _contains "$_res" "already exists"; then - _info "Record already exists, skipping." - return 0 - fi - - _nres="$(printf '%s' "$_res" | _normalizeJson)" - if [ "$?" -ne 0 ] || [ -z "$_nres" ]; then - _nres="$_res" - fi - - if _contains "$_nres" "\"status\":4" || _contains "$_nres" "\"status\":5" || _contains "$_nres" "\"errors\""; then - _err "API error: $_res" - return 1 - fi - - return 0 -} - -dns_czechia_rm() { - fulldomain="$1" - txtvalue="$2" - - _debug "dns_czechia_rm fulldomain='$fulldomain'" - - if [ -z "$fulldomain" ] || [ -z "$txtvalue" ]; then - _err "dns_czechia_rm: missing fulldomain or txtvalue" - return 1 - fi - - _czechia_load_conf || return 1 - - _current_zone=$(_czechia_pick_zone "$fulldomain") - if [ -z "$_current_zone" ]; then - _err "No matching zone found for $fulldomain. Please check CZ_Zones configuration." - return 1 - fi - - _czechia_tab="$(printf '\t')" - _cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//") - _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//") - - if [ -z "$_cz" ] || [ -z "$_tk" ]; then - _err "Missing zone or CZ_AuthorizationToken." - return 1 - fi - - _url="$CZ_API_BASE/api/DNS/$_cz/TXT" - _fd=$(printf "%s" "$fulldomain" | _lower_case | sed 's/\.$//') - - if [ "$_fd" = "$_cz" ]; then - _h="@" - else - _h=$(printf "%s" "$_fd" | sed "s/\.$_cz$//") - [ "$_h" = "$_fd" ] && _h="@" - fi - [ -z "$_h" ] && _h="@" - - _h_esc=$(printf "%s" "$_h" | sed 's/\\/\\\\/g; s/"/\\"/g') - _txt_esc=$(printf "%s" "$txtvalue" | sed 's/\\/\\\\/g; s/"/\\"/g') - _body="{\"hostName\":\"$_h_esc\",\"text\":\"$_txt_esc\",\"ttl\":300,\"publishZone\":1}" - - _debug "URL: $_url" - _debug "Body: $_body" - - export _H1="Content-Type: application/json" - export _H2="AuthorizationToken: $_tk" - - _res="$(_post "$_body" "$_url" "" "DELETE")" - _post_exit="$?" - _debug2 "Response: $_res" - - if [ "$_post_exit" -ne 0 ]; then - _err "CZECHIA DNS API DELETE request failed for $_fd: exit code $_post_exit, response: $_res" - return 1 - fi - - _res_normalized=$(printf '%s' "$_res" | _normalizeJson) - - if _contains "$_res_normalized" '"isError":true'; then - _err "CZECHIA DNS API reported an error while deleting TXT for $_fd: $_res" - return 1 - fi - - return 0 -} - -_czechia_load_conf() { - CZ_AuthorizationToken="${CZ_AuthorizationToken:-$(_readaccountconf_mutable CZ_AuthorizationToken)}" - if [ -z "$CZ_AuthorizationToken" ]; then - _err "Missing CZ_AuthorizationToken" - return 1 - fi - - CZ_Zones="${CZ_Zones:-$(_readaccountconf_mutable CZ_Zones)}" - if [ -z "$CZ_Zones" ]; then - _err "Missing CZ_Zones" - return 1 - fi - - CZ_API_BASE="${CZ_API_BASE:-$(_readaccountconf_mutable CZ_API_BASE)}" - [ -z "$CZ_API_BASE" ] && CZ_API_BASE="https://api.czechia.com" - - _saveaccountconf_mutable CZ_AuthorizationToken "$CZ_AuthorizationToken" - _saveaccountconf_mutable CZ_Zones "$CZ_Zones" - _saveaccountconf_mutable CZ_API_BASE "$CZ_API_BASE" - - return 0 -} - -_czechia_pick_zone() { - _czechia_pz_tab="$(printf '\t')" - _fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//') - _best_zone="" - - _zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g') - for _z in $_zones_space; do - _clean_z=$(printf "%s" "$_z" | _lower_case | sed "s/[ $_czechia_pz_tab]//g; s/\.\$//") - [ -z "$_clean_z" ] && continue - - case "$_fd" in - "$_clean_z" | *."$_clean_z") - if [ ${#_clean_z} -gt ${#_best_zone} ]; then - _best_zone="$_clean_z" - fi - ;; - esac - done - - printf "%s" "$_best_zone" -} diff --git a/dnsapi/dns_da.sh b/dnsapi/dns_da.sh index d9cf6247..36251b05 100755 --- a/dnsapi/dns_da.sh +++ b/dnsapi/dns_da.sh @@ -4,7 +4,7 @@ dns_da_info='DirectAdmin Server API Site: DirectAdmin.com/api.php Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_da Options: - DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443". Special characters in the user/password must be percent-encoded, e.g. "@" -> "%40". + DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443" DA_Api_Insecure Insecure TLS. 0: check for cert validity, 1: always accept Issues: github.com/TigerP/acme.sh/issues ' diff --git a/dnsapi/dns_ddnss.sh b/dnsapi/dns_ddnss.sh index 0ac353d4..118b148b 100644 --- a/dnsapi/dns_ddnss.sh +++ b/dnsapi/dns_ddnss.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_ddnss Options: DDNSS_Token API Token Issues: github.com/acmesh-official/acme.sh/issues/2230 -Author: @helbgd, @mod242 +Author: RaidenII, helbgd, mod242 ' DDNSS_DNS_API="https://ddnss.de/upd.php" diff --git a/dnsapi/dns_desec.sh b/dnsapi/dns_desec.sh index e5e4809a..d6b9c355 100644 --- a/dnsapi/dns_desec.sh +++ b/dnsapi/dns_desec.sh @@ -4,7 +4,7 @@ dns_desec_info='deSEC.io Site: desec.readthedocs.io/en/latest/ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_desec Options: - DEDYN_TOKEN API Token + DDNSS_Token API Token Issues: github.com/acmesh-official/acme.sh/issues/2180 Author: Zheng Qian ' @@ -39,7 +39,6 @@ dns_desec_add() { _err "invalid domain" return 1 fi - _sub_domain=$(echo "$_sub_domain" | _lower_case) _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" @@ -49,7 +48,7 @@ dns_desec_add() { _desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/" if [ "$_code" = "200" ]; then - oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")" + oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")" _debug "existing TXT found" _debug oldtxtvalues "$oldtxtvalues" if [ -n "$oldtxtvalues" ]; then @@ -101,7 +100,7 @@ dns_desec_rm() { _err "invalid domain" return 1 fi - _sub_domain=$(echo "$_sub_domain" | _lower_case) + _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" @@ -111,7 +110,7 @@ dns_desec_rm() { _desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/" if [ "$_code" = "200" ]; then - oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")" + oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")" _debug "existing TXT found" _debug oldtxtvalues "$oldtxtvalues" if [ -n "$oldtxtvalues" ]; then @@ -151,8 +150,6 @@ _desec_rest() { if [ "$m" != "GET" ]; then _secure_debug2 data "$data" response="$(_post "$data" "$ep" "" "$m")" - _info "Sleeping 1s to respect deSEC write rate limit" - _sleep 1 else response="$(_get "$ep")" fi diff --git a/dnsapi/dns_dnsexit.sh b/dnsapi/dns_dnsexit.sh index b92482cf..ec3b07a4 100644 --- a/dnsapi/dns_dnsexit.sh +++ b/dnsapi/dns_dnsexit.sh @@ -5,11 +5,14 @@ Site: DNSExit.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsexit Options: DNSEXIT_API_KEY API Key + DNSEXIT_AUTH_USER Username + DNSEXIT_AUTH_PASS Password Issues: github.com/acmesh-official/acme.sh/issues/4719 Author: Samuel Jimenez ' DNSEXIT_API_URL="https://api.dnsexit.com/dns/" +DNSEXIT_HOSTS_URL="https://update.dnsexit.com/ipupdate/hosts.jsp" ######## Public functions ##################### #Usage: dns_dnsexit_add _acme-challenge.*.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" @@ -25,7 +28,20 @@ dns_dnsexit_add() { return 1 fi - _dnsexit_zone_op add ',"ttl":1,"overwrite":false' + _debug 'First detect the root zone' + if ! _get_root "$fulldomain"; then + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"add\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":0,\"overwrite\":false}}"; then + _err "$response" + return 1 + fi + + _debug2 _response "$response" + return 0 } #Usage: fulldomain txtvalue @@ -42,43 +58,54 @@ dns_dnsexit_rm() { return 1 fi - _dnsexit_zone_op delete '' + _debug 'First detect the root zone' + if ! _get_root "$fulldomain"; then + _err "$response" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"delete\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"}}"; then + _err "$response" + return 1 + fi + + _debug2 _response "$response" + return 0 } #################### Private functions below ################################## -# The legacy zone-detection endpoint (update.dnsexit.com/ipupdate/hosts.jsp) -# was shut down by DNSExit and now returns 503, and the JSON API offers no -# zone-list call. So find the root zone by attempting the actual operation at -# each domain level: the API answers "code":0 only when the domain matches a -# zone of the account. https://github.com/acmesh-official/acme.sh/issues/6914 -#Usage: _dnsexit_zone_op -_dnsexit_zone_op() { - _op="$1" - _extra="$2" +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 i=1 while true; do - _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100) - _debug _domain "$_domain" + _domain=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug h "$_domain" if [ -z "$_domain" ]; then - _err "Could not find the root zone of $fulldomain in your DNSExit account" return 1 fi - _sub_domain="$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//")" - if [ "$_sub_domain" = "$fulldomain" ]; then - _sub_domain="" - fi - _debug _sub_domain "$_sub_domain" + _debug login "$DNSEXIT_AUTH_USER" + _debug password "$DNSEXIT_AUTH_PASS" + _debug domain "$_domain" - if _dnsexit_rest "{\"domain\":\"$_domain\",\"$_op\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"$_extra}}"; then - if _contains "$response" "\"code\":0" || _contains "$response" "\"code\": 0"; then - _debug2 _response "$response" - return 0 - fi - _debug "Zone $_domain was not accepted, trying the next level" "$response" + _dnsexit_http "login=$DNSEXIT_AUTH_USER&password=$DNSEXIT_AUTH_PASS&domain=$_domain" + + if _contains "$response" "0=$_domain"; then + _sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")" + return 0 + else + _debug "Go to next level of $_domain" fi i=$(_math "$i" + 1) done + + return 1 } _dnsexit_rest() { @@ -109,7 +136,27 @@ _dnsexit_rest() { return 0 } +_dnsexit_http() { + m=GET + param="$1" + _debug param "$param" + _debug get "$DNSEXIT_HOSTS_URL?$param" + + response="$(_get "$DNSEXIT_HOSTS_URL?$param")" + + _debug response "$response" + + if [ "$?" != "0" ]; then + _err "Error $param" + return 1 + fi + + _debug2 response "$response" + return 0 +} + get_account_info() { + DNSEXIT_API_KEY="${DNSEXIT_API_KEY:-$(_readaccountconf_mutable DNSEXIT_API_KEY)}" if test -z "$DNSEXIT_API_KEY"; then DNSEXIT_API_KEY='' @@ -119,5 +166,23 @@ get_account_info() { _saveaccountconf_mutable DNSEXIT_API_KEY "$DNSEXIT_API_KEY" + DNSEXIT_AUTH_USER="${DNSEXIT_AUTH_USER:-$(_readaccountconf_mutable DNSEXIT_AUTH_USER)}" + if test -z "$DNSEXIT_AUTH_USER"; then + DNSEXIT_AUTH_USER="" + _err 'DNSEXIT_AUTH_USER was not exported' + return 1 + fi + + _saveaccountconf_mutable DNSEXIT_AUTH_USER "$DNSEXIT_AUTH_USER" + + DNSEXIT_AUTH_PASS="${DNSEXIT_AUTH_PASS:-$(_readaccountconf_mutable DNSEXIT_AUTH_PASS)}" + if test -z "$DNSEXIT_AUTH_PASS"; then + DNSEXIT_AUTH_PASS="" + _err 'DNSEXIT_AUTH_PASS was not exported' + return 1 + fi + + _saveaccountconf_mutable DNSEXIT_AUTH_PASS "$DNSEXIT_AUTH_PASS" + return 0 } diff --git a/dnsapi/dns_dnshome.sh b/dnsapi/dns_dnshome.sh index 6d583246..59828796 100755 --- a/dnsapi/dns_dnshome.sh +++ b/dnsapi/dns_dnshome.sh @@ -7,7 +7,7 @@ Options: DNSHOME_Subdomain Subdomain DNSHOME_SubdomainPassword Subdomain Password Issues: github.com/acmesh-official/acme.sh/issues/3819 -Author: @dnsHome-de +Author: dnsHome.de https://github.com/dnsHome-de ' # Usage: add subdomain.ddnsdomain.tld "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" diff --git a/dnsapi/dns_dnsimple.sh b/dnsapi/dns_dnsimple.sh index 257549b4..10a3821d 100644 --- a/dnsapi/dns_dnsimple.sh +++ b/dnsapi/dns_dnsimple.sh @@ -5,7 +5,6 @@ Site: DNSimple.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple Options: DNSimple_OAUTH_TOKEN OAuth Token - DNSimple_ACCOUNT_ID Account ID. Optional, only needed when the token can access multiple accounts. Issues: github.com/pho3nixf1re/acme.sh/issues ' @@ -18,7 +17,6 @@ dns_dnsimple_add() { fulldomain=$1 txtvalue=$2 - DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}" if [ -z "$DNSimple_OAUTH_TOKEN" ]; then DNSimple_OAUTH_TOKEN="" _err "You have not set the dnsimple oauth token yet." @@ -27,10 +25,10 @@ dns_dnsimple_add() { fi # save the oauth token for later - _saveaccountconf_mutable DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN" + _saveaccountconf DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN" if ! _get_account_id; then - _err "failed to retrieve account id" + _err "failed to retrive account id" return 1 fi @@ -58,14 +56,8 @@ dns_dnsimple_add() { dns_dnsimple_rm() { fulldomain=$1 - DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}" - if [ -z "$DNSimple_OAUTH_TOKEN" ]; then - _err "You have not set the dnsimple oauth token yet." - return 1 - fi - if ! _get_account_id; then - _err "failed to retrieve account id" + _err "failed to retrive account id" return 1 fi @@ -130,16 +122,13 @@ _get_root() { # returns _account_id _get_account_id() { - DNSimple_ACCOUNT_ID="${DNSimple_ACCOUNT_ID:-$(_readaccountconf_mutable DNSimple_ACCOUNT_ID)}" - if [ "$DNSimple_ACCOUNT_ID" ]; then - _saveaccountconf_mutable DNSimple_ACCOUNT_ID "$DNSimple_ACCOUNT_ID" - _account_id="$DNSimple_ACCOUNT_ID" - _debug _account_id "$_account_id" - return 0 + _debug "retrive account id" + if ! _dnsimple_rest GET "whoami"; then + return 1 fi - _debug "retrieve account id" - if ! _dnsimple_rest GET "whoami"; then + if _contains "$response" "\"account\":null"; then + _err "no account associated with this token" return 1 fi @@ -148,25 +137,7 @@ _get_account_id() { return 1 fi - if _contains "$response" "\"account\":null"; then - # the whoami of a user token (dnsimple_u_*) carries no account, - # so list the accounts the token can access instead - # https://github.com/acmesh-official/acme.sh/issues/6491 - if ! _dnsimple_rest GET "accounts"; then - return 1 - fi - fi - _account_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"email\":" | cut -d: -f2 | cut -d, -f1) - if [ -z "$_account_id" ]; then - _err "no account associated with this token" - return 1 - fi - if [ "$(echo "$_account_id" | wc -l)" -gt 1 ]; then - _err "The token has access to multiple accounts, please pick one and set it explicitly:" - _err "export DNSimple_ACCOUNT_ID=" - return 1 - fi _debug _account_id "$_account_id" return 0 diff --git a/dnsapi/dns_duckdns.sh b/dnsapi/dns_duckdns.sh index 33d401b0..71594873 100755 --- a/dnsapi/dns_duckdns.sh +++ b/dnsapi/dns_duckdns.sh @@ -5,7 +5,7 @@ Site: www.DuckDNS.org Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_duckdns Options: DuckDNS_Token API Token -Author: @RaidenII +Author: RaidenII ' DuckDNS_API="https://www.duckdns.org/update" diff --git a/dnsapi/dns_dyn.sh b/dnsapi/dns_dyn.sh index 9b1a97a2..94201923 100644 --- a/dnsapi/dns_dyn.sh +++ b/dnsapi/dns_dyn.sh @@ -8,7 +8,7 @@ Options: DYN_Customer Customer DYN_Username API Username DYN_Password Secret -Author: Gerd Naschenweng <@magicdude4eva> +Author: Gerd Naschenweng ' # Dyn Managed DNS API diff --git a/dnsapi/dns_dynu.sh b/dnsapi/dns_dynu.sh index 3ac5c2f1..1d1fc311 100644 --- a/dnsapi/dns_dynu.sh +++ b/dnsapi/dns_dynu.sh @@ -23,8 +23,6 @@ dns_dynu_add() { fulldomain=$1 txtvalue=$2 - Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}" - Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}" if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then Dynu_ClientId="" Dynu_Secret="" @@ -34,8 +32,8 @@ dns_dynu_add() { fi #save the client id and secret to the account conf file. - _saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId" - _saveaccountconf_mutable Dynu_Secret "$Dynu_Secret" + _saveaccountconf Dynu_ClientId "$Dynu_ClientId" + _saveaccountconf Dynu_Secret "$Dynu_Secret" if [ -z "$Dynu_Token" ]; then _info "Getting Dynu token." @@ -71,8 +69,6 @@ dns_dynu_rm() { fulldomain=$1 txtvalue=$2 - Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}" - Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}" if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then Dynu_ClientId="" Dynu_Secret="" @@ -82,8 +78,8 @@ dns_dynu_rm() { fi #save the client id and secret to the account conf file. - _saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId" - _saveaccountconf_mutable Dynu_Secret "$Dynu_Secret" + _saveaccountconf Dynu_ClientId "$Dynu_ClientId" + _saveaccountconf Dynu_Secret "$Dynu_Secret" if [ -z "$Dynu_Token" ]; then _info "Getting Dynu token." @@ -218,11 +214,11 @@ _dynu_authentication() { response="$(_get "$Dynu_EndPoint/oauth2/token")" if [ "$?" != "0" ]; then - _err "Authentication failed: no response from $Dynu_EndPoint/oauth2/token" + _err "Authentication failed." return 1 fi if _contains "$response" "Authentication Exception"; then - _err "Authentication failed. Server response: $response" + _err "Authentication failed." return 1 fi if _contains "$response" "access_token"; then diff --git a/dnsapi/dns_dynv6.sh b/dnsapi/dns_dynv6.sh index 3e7ce8d6..76af17f5 100644 --- a/dnsapi/dns_dynv6.sh +++ b/dnsapi/dns_dynv6.sh @@ -8,7 +8,7 @@ Options: OptionsAlt: KEY Path to SSH private key file. E.g. "/root/.ssh/dynv6" Issues: github.com/acmesh-official/acme.sh/issues/2702 -Author: @StefanAbl +Author: StefanAbl ' dynv6_api="https://dynv6.com/api/v2" @@ -107,7 +107,7 @@ _get_domain() { return 0 fi done - _err "Either there is no such host on your dynv6 account, or it cannot be accessed with this key" + _err "Either their is no such host on your dnyv6 account or it cannot be accessed with this key" return 1 } @@ -179,8 +179,8 @@ _dns_dynv6_rm_http() { fi } -#Usage: _get_zone_id $record #get the zoneid for a specifc record or zone +#usage: _get_zone_id §record #where $record is the record to get the id for #returns _zone_id the id of the zone _get_zone_id() { @@ -189,6 +189,7 @@ _get_zone_id() { _dynv6_rest GET zones zones="$(echo "$response" | tr '}' '\n' | tr ',' '\n' | grep name | sed 's/\[//g' | tr -d '{' | tr -d '"')" + #echo $zones selected="" for z in $zones; do @@ -216,9 +217,9 @@ _get_zone_name() { _zone_name="${_zone_name#name:}" } -#usage _get_record_id $zone_id $record -# where zone_id is the value returned by _get_zone_id -# and record is in the form _acme.www for an fqdn of _acme.www.example.com +#usaage _get_record_id $zone_id $record +# where zone_id is thevalue returned by _get_zone_id +# and record ist in the form _acme.www for an fqdn of _acme.www.example.com # returns _record_id _get_record_id() { _zone_id="$1" @@ -233,7 +234,8 @@ _get_record_id() { _get_record_id_from_response() { response="$1" - _record_id="$(echo "$response" | tr '}' '\n' | grep "\"name\":\"$record\"" | grep "\"data\":\"$value\"" | tr ',' '\n' | grep '"id":' | tr -d '"' | tr -d 'id:' | tr -d '{')" + _record_id="$(echo "$response" | tr '}' '\n' | grep "\"name\":\"$record\"" | grep "\"data\":\"$value\"" | tr ',' '\n' | grep id | tr -d '"' | tr -d 'id:')" + #_record_id="${_record_id#id:}" if [ -z "$_record_id" ]; then _err "no such record: $record found in zone $_zone_id" return 1 diff --git a/dnsapi/dns_easydns.sh b/dnsapi/dns_easydns.sh index 423def2b..1c96ac8f 100644 --- a/dnsapi/dns_easydns.sh +++ b/dnsapi/dns_easydns.sh @@ -7,7 +7,7 @@ Options: EASYDNS_Token API Token EASYDNS_Key API Key Issues: github.com/acmesh-official/acme.sh/issues/2647 -Author: @Neilpang, wurzelpanzer +Author: Neilpang, wurzelpanzer ' # API Documentation: https://sandbox.rest.easydns.net:3001/ diff --git a/dnsapi/dns_edgecenter.sh b/dnsapi/dns_edgecenter.sh index 8f4ad171..cdd150df 100644 --- a/dnsapi/dns_edgecenter.sh +++ b/dnsapi/dns_edgecenter.sh @@ -1,13 +1,13 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 -dns_edgecenter_info='EdgeCenter.ru -Site: EdgeCenter.ru -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_edgecenter + +# EdgeCenter DNS API integration for acme.sh +# Author: Konstantin Ruchev +dns_edgecenter_info='edgecenter DNS API +Site: https://edgecenter.ru +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_edgecenter Options: - EDGECENTER_API_KEY API Key -Issues: github.com/acmesh-official/acme.sh/issues/6313 -Author: Konstantin Ruchev -' + EDGECENTER_API_KEY auth APIKey' EDGECENTER_API="https://api.edgecenter.ru" DOMAIN_TYPE= diff --git a/dnsapi/dns_edgedns.sh b/dnsapi/dns_edgedns.sh index 9ff1cc06..e88a1483 100755 --- a/dnsapi/dns_edgedns.sh +++ b/dnsapi/dns_edgedns.sh @@ -363,12 +363,17 @@ _edgedns_rest() { _edgedns_eg_timestamp() { _debug "Generating signature Timestamp" - #Akamai accepts a clock skew of +/-30s, so use the system clock directly. - #The previous code fetched the Date header from www.ntp.org, which is not - #a reliable time source (it served a wrong time for hours, issue 3973), - #cost an extra https round-trip for every API request, and combined the - #remote time of day with the LOCAL date, breaking around UTC midnight. - _eg_timestamp="$(date -u "+%Y%m%dT%H:%M:%S+0000")" + _debug3 "Retriving ntp time" + _timeheaders="$(_get "https://www.ntp.org" "onlyheader")" + _debug3 "_timeheaders" "$_timeheaders" + _ntpdate="$(echo "$_timeheaders" | grep -i "Date:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n")" + _debug3 "_ntpdate" "$_ntpdate" + _ntpdate="$(echo "${_ntpdate}" | sed -e 's/^[[:space:]]*//')" + _debug3 "_NTPDATE" "$_ntpdate" + _ntptime="$(echo "${_ntpdate}" | _head_n 1 | cut -d " " -f 5 | tr -d "\r\n")" + _debug3 "_ntptime" "$_ntptime" + _eg_timestamp=$(date -u "+%Y%m%dT") + _eg_timestamp="$(printf "%s%s+0000" "$_eg_timestamp" "$_ntptime")" _debug "_eg_timestamp" "$_eg_timestamp" } diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh deleted file mode 100755 index a485849a..00000000 --- a/dnsapi/dns_efficientip.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_efficientip_info='efficientip.com -Site: https://efficientip.com/ -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_efficientip -Options: - EfficientIP_Creds HTTP Basic Authentication credentials. E.g. "username:password" - EfficientIP_Server EfficientIP SOLIDserver Management IP address or FQDN. - EfficientIP_DNS_Name Name of the DNS smart or server hosting the zone. Optional. - EfficientIP_View Name of the DNS view hosting the zone. Optional. -OptionsAlt: - EfficientIP_Token_Key Alternative API token key, prefered over basic authentication. - EfficientIP_Token_Secret Alternative API token secret, required when using a token key. - EfficientIP_Server EfficientIP SOLIDserver Management IP address or FQDN. - EfficientIP_DNS_Name Name of the DNS smart or server hosting the zone. Optional. - EfficientIP_View Name of the DNS view hosting the zone. Optional. -Issues: github.com/acmesh-official/acme.sh/issues/6325 -Author: EfficientIP-Labs -' - -dns_efficientip_add() { - fulldomain=$1 - txtvalue=$2 - - _info "Using EfficientIP API" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - if { [ -z "${EfficientIP_Creds}" ] && { [ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ]; }; } || [ -z "${EfficientIP_Server}" ]; then - EfficientIP_Creds="" - EfficientIP_Token_Key="" - EfficientIP_Token_Secret="" - EfficientIP_Server="" - _err "You didn't specify any EfficientIP credentials or token or server (EfficientIP_Creds; EfficientIP_Token_Key; EfficientIP_Token_Secret; EfficientIP_Server)." - _err "Please set them via EXPORT EfficientIP_Creds=username:password or EXPORT EfficientIP_server=ip/hostname" - _err "or if you want to use Token instead EXPORT EfficientIP_Token_Key=yourkey" - _err "and EXPORT EfficientIP_Token_Secret=yoursecret" - _err "then try again." - return 1 - fi - - if [ -z "${EfficientIP_DNS_Name}" ]; then - EfficientIP_DNS_Name="" - fi - - EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode) - - if [ -z "${EfficientIP_View}" ]; then - EfficientIP_View="" - fi - - EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode) - - _saveaccountconf EfficientIP_Creds "${EfficientIP_Creds}" - _saveaccountconf EfficientIP_Token_Key "${EfficientIP_Token_Key}" - _saveaccountconf EfficientIP_Token_Secret "${EfficientIP_Token_Secret}" - _saveaccountconf EfficientIP_Server "${EfficientIP_Server}" - _saveaccountconf EfficientIP_DNS_Name "${EfficientIP_DNS_Name}" - _saveaccountconf EfficientIP_View "${EfficientIP_View}" - - export _H1="Accept-Language:en-US" - baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?rr_type=TXT&rr_ttl=300&rr_name=${fulldomain}&rr_value1=${txtvalue}" - - if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then - baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}" - fi - - if [ "${EfficientIP_ViewEncoded}" != "" ]; then - baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}" - fi - - if [ -z "${EfficientIP_Token_Secret}" ] || [ -z "${EfficientIP_Token_Key}" ]; then - EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64) - export _H2="Authorization: Basic ${EfficientIP_CredsEncoded}" - else - TS=$(date +%s) - Sig=$(printf "%b\n$TS\nPOST\n$baseurlnObject" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex) - EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig") - export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}" - export _H3="X-SDS-TS: ${TS}" - fi - - result="$(_post "" "${baseurlnObject}" "" "POST")" - - if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then - _info "DNS record successfully created" - return 0 - else - _err "Error creating DNS record" - _err "${result}" - return 1 - fi -} - -dns_efficientip_rm() { - fulldomain=$1 - txtvalue=$2 - - _info "Using EfficientIP API" - _debug fulldomain "${fulldomain}" - _debug txtvalue "${txtvalue}" - - EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode) - EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode) - EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64) - - export _H1="Accept-Language:en-US" - - baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_delete?rr_type=TXT&rr_name=$fulldomain&rr_value1=$txtvalue" - if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then - baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}" - fi - - if [ "${EfficientIP_ViewEncoded}" != "" ]; then - baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}" - fi - - if [ -z "$EfficientIP_Token_Secret" ] || [ -z "$EfficientIP_Token_Key" ]; then - EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64) - export _H2="Authorization: Basic $EfficientIP_CredsEncoded" - else - TS=$(date +%s) - Sig=$(printf "%b\n$TS\nDELETE\n${baseurlnObject}" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex) - EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig") - export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}" - export _H3="X-SDS-TS: $TS" - fi - - result="$(_post "" "${baseurlnObject}" "" "DELETE")" - - if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then - _info "DNS Record successfully deleted" - return 0 - else - _err "Error deleting DNS record" - _err "${result}" - return 1 - fi -} diff --git a/dnsapi/dns_eurodns.sh b/dnsapi/dns_eurodns.sh deleted file mode 100644 index 0fac4cb5..00000000 --- a/dnsapi/dns_eurodns.sh +++ /dev/null @@ -1,267 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_eurodns_info='EuroDNS -Site: eurodns.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_eurodns -Options: - EURODNS_APP_ID Application ID - EURODNS_API_KEY API Key - EURODNS_TTL TTL. Default: "600". -Issues: github.com/acmesh-official/acme.sh/issues -Author: Nicolas Santorelli -' - -# -# EuroDNS DNS API -# -# EuroDNS API documentation: -# https://docapi.eurodns.com -# -# Usage: -# export EURODNS_APP_ID="your-app-id" -# export EURODNS_API_KEY="your-api-key" -# acme.sh --issue --dns dns_eurodns -d example.com -d *.example.com -# -# The credentials will be saved in ~/.acme.sh/account.conf -# -# Optional: -# export EURODNS_API_URL="https://rest-api.eurodns.com" # Default API URL -# export EURODNS_TTL=600 # Default TTL (minimum 600 for EuroDNS) -# - -EURODNS_API_DEFAULT="https://rest-api.eurodns.com" -EURODNS_TTL_DEFAULT=600 - -######## Public functions ##################### - -#Usage: dns_eurodns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_eurodns_add() { - fulldomain="$(echo "$1" | _lower_case)" - txtvalue=$2 - - _info "Using EuroDNS DNS API" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}" - EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}" - EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}" - EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}" - EURODNS_TTL="${EURODNS_TTL:-$(_readaccountconf_mutable EURODNS_TTL)}" - EURODNS_TTL="${EURODNS_TTL:-$EURODNS_TTL_DEFAULT}" - - if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then - EURODNS_APP_ID="" - EURODNS_API_KEY="" - _err "You didn't specify EuroDNS App ID and API Key." - _err "Please export EURODNS_APP_ID and EURODNS_API_KEY and try again." - return 1 - fi - - _saveaccountconf_mutable EURODNS_APP_ID "$EURODNS_APP_ID" - _saveaccountconf_mutable EURODNS_API_KEY "$EURODNS_API_KEY" - if [ "$EURODNS_API_URL" != "$EURODNS_API_DEFAULT" ]; then - _saveaccountconf_mutable EURODNS_API_URL "$EURODNS_API_URL" - fi - if [ "$EURODNS_TTL" != "$EURODNS_TTL_DEFAULT" ]; then - _saveaccountconf_mutable EURODNS_TTL "$EURODNS_TTL" - fi - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "Invalid domain" - return 1 - fi - _debug _domain "$_domain" - _debug _sub_domain "$_sub_domain" - - _info "Adding TXT record" - if _eurodns_add_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then - _info "Added TXT record successfully." - return 0 - else - _err "Failed to add TXT record." - return 1 - fi -} - -#Usage: fulldomain txtvalue -dns_eurodns_rm() { - fulldomain="$(echo "$1" | _lower_case)" - txtvalue=$2 - - _info "Using EuroDNS DNS API" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}" - EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}" - EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}" - EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}" - - if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then - EURODNS_APP_ID="" - EURODNS_API_KEY="" - _err "You didn't specify EuroDNS App ID and API Key." - return 1 - fi - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "Invalid domain" - return 1 - fi - _debug _domain "$_domain" - _debug _sub_domain "$_sub_domain" - - _info "Removing TXT record" - if _eurodns_rm_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then - _info "Removed TXT record successfully." - return 0 - else - _err "Failed to remove TXT record." - return 1 - fi -} - -#################### Private functions below ################################## - -# _sub_domain=_acme-challenge.www -# _domain=domain.com -_get_root() { - domain=$1 - i=1 - p=1 - - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug h "$h" - if [ -z "$h" ]; then - return 1 - fi - - _eurodns_rest GET "dns-zones/$h" - if [ "$?" != "0" ]; then - if [ "$_code" = "404" ]; then - _debug "Zone $h not found, continuing..." - else - _err "API error looking up zone $h" - return 1 - fi - p=$i - i=$(_math "$i" + 1) - continue - fi - - if _contains "$response" '"name"'; then - if [ "$i" = "1" ]; then - _sub_domain="@" - else - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - fi - _domain=$h - return 0 - fi - - p=$i - i=$(_math "$i" + 1) - done - - return 1 -} - -_eurodns_add_txt_record() { - domain=$1 - subdomain=$2 - txtvalue=$3 - - data='[{"type":"TXT","host":"'"$subdomain"'","rdata":"'"$txtvalue"'","ttl":'"$EURODNS_TTL"'}]' - - _debug "Adding TXT record via API" - if _eurodns_rest POST "dns-zones/$domain/dns-records" "$data"; then - if _contains "$response" "$txtvalue"; then - return 0 - fi - fi - _err "Failed to add TXT record" - return 1 -} - -_eurodns_rm_txt_record() { - domain=$1 - subdomain=$2 - txtvalue=$3 - - _debug "Getting current zone data for $domain" - - if ! _eurodns_rest GET "dns-zones/$domain"; then - _err "Failed to get zone data" - return 1 - fi - - zone_data=$(echo "$response" | _normalizeJson) - _debug2 zone_data "$zone_data" - - # Find the record ID matching our TXT record - record_id=$(echo "$zone_data" | tr '{' '\n' | grep -F '"TXT"' | grep -F "\"$subdomain\"" | grep -F "\"$txtvalue\"" | _egrep_o '"id" *: *[0-9]+' | cut -d : -f 2 | _head_n 1) - _debug record_id "$record_id" - - if [ -z "$record_id" ]; then - _info "TXT record not found or already removed" - return 0 - fi - - _debug "Deleting TXT record $record_id" - if ! _eurodns_rest DELETE "dns-zones/$domain/dns-records/$record_id"; then - _err "Failed to delete TXT record" - return 1 - fi - - return 0 -} - -# Usage: _eurodns_rest METHOD ENDPOINT [DATA] -_eurodns_rest() { - method=$1 - endpoint=$2 - data="$3" - - export _H1="X-APP-ID: $EURODNS_APP_ID" - export _H2="X-API-KEY: $EURODNS_API_KEY" - export _H3="Content-Type: application/json" - - url="$EURODNS_API_URL/$endpoint" - - _debug2 url "$url" - _debug2 method "$method" - _debug2 data "$data" - - : >"$HTTP_HEADER" - - if [ "$method" = "GET" ]; then - response="$(_get "$url")" - else - response="$(_post "$data" "$url" "" "$method")" - fi - - _ret="$?" - unset _H1 _H2 _H3 - _debug2 response "$response" - - _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" - _debug2 _code "$_code" - - if [ "$_ret" != "0" ]; then - _err "Error calling API: $endpoint" - return 1 - fi - - if [ "$_code" != "200" ] && [ "$_code" != "201" ] && [ "$_code" != "204" ]; then - if [ "$_code" != "404" ]; then - _err "API error (HTTP $_code): $response" - fi - return 1 - fi - - return 0 -} diff --git a/dnsapi/dns_exoscale.sh b/dnsapi/dns_exoscale.sh old mode 100644 new mode 100755 index ddd526a4..6898ce38 --- a/dnsapi/dns_exoscale.sh +++ b/dnsapi/dns_exoscale.sh @@ -8,9 +8,9 @@ Options: EXOSCALE_SECRET_KEY API Secret key ' -EXOSCALE_API="https://api-ch-gva-2.exoscale.com/v2" +EXOSCALE_API=https://api.exoscale.com/dns/v1 -######## Public functions ######## +######## Public functions ##################### # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" # Used to add txt record @@ -18,197 +18,159 @@ dns_exoscale_add() { fulldomain=$1 txtvalue=$2 - _debug "Using Exoscale DNS v2 API" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - if ! _check_auth; then + if ! _checkAuth; then return 1 fi - root_domain_id=$(_get_root_domain_id "$fulldomain") - if [ -z "$root_domain_id" ]; then - _err "Unable to determine root domain ID for $fulldomain" + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" return 1 fi - _debug root_domain_id "$root_domain_id" - # Always get the subdomain part first - sub_domain=$(_get_sub_domain "$fulldomain" "$root_domain_id") - _debug sub_domain "$sub_domain" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" - # Build the record name properly - if [ -z "$sub_domain" ]; then - record_name="_acme-challenge" - else - record_name="_acme-challenge.$sub_domain" + _info "Adding record" + if _exoscale_rest POST "domains/$_domain_id/records" "{\"record\":{\"name\":\"$_sub_domain\",\"record_type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}}" "$_domain_token"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + fi fi + _err "Add txt record error." + return 1 - payload=$(printf '{"name":"%s","type":"TXT","content":"%s","ttl":120}' "$record_name" "$txtvalue") - _debug payload "$payload" - - response=$(_exoscale_rest POST "/dns-domain/${root_domain_id}/record" "$payload") - if _contains "$response" "\"id\""; then - _info "TXT record added successfully." - return 0 - else - _err "Error adding TXT record: $response" - return 1 - fi } +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation dns_exoscale_rm() { fulldomain=$1 + txtvalue=$2 - _debug "Using Exoscale DNS v2 API for removal" - _debug fulldomain "$fulldomain" - - if ! _check_auth; then + if ! _checkAuth; then return 1 fi - root_domain_id=$(_get_root_domain_id "$fulldomain") - if [ -z "$root_domain_id" ]; then - _err "Unable to determine root domain ID for $fulldomain" + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" return 1 fi - record_name="_acme-challenge" - sub_domain=$(_get_sub_domain "$fulldomain" "$root_domain_id") - if [ -n "$sub_domain" ]; then - record_name="_acme-challenge.$sub_domain" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _exoscale_rest GET "domains/${_domain_id}/records?type=TXT&name=$_sub_domain" "" "$_domain_token" + if _contains "$response" "\"name\":\"$_sub_domain\"" >/dev/null; then + _record_id=$(echo "$response" | tr '{' "\n" | grep "\"content\":\"$txtvalue\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \") fi - record_id=$(_find_record_id "$root_domain_id" "$record_name") - if [ -z "$record_id" ]; then - _err "TXT record not found for deletion." + if [ -z "$_record_id" ]; then + _err "Can not get record id to remove." return 1 fi - response=$(_exoscale_rest DELETE "/dns-domain/$root_domain_id/record/$record_id") - if _contains "$response" "\"state\":\"success\""; then - _info "TXT record deleted successfully." - return 0 - else - _err "Error deleting TXT record: $response" + _debug "Deleting record $_record_id" + + if ! _exoscale_rest DELETE "domains/$_domain_id/records/$_record_id" "" "$_domain_token"; then + _err "Delete record error." return 1 fi -} -######## Private helpers ######## - -_check_auth() { - EXOSCALE_API_KEY="${EXOSCALE_API_KEY:-$(_readaccountconf_mutable EXOSCALE_API_KEY)}" - EXOSCALE_SECRET_KEY="${EXOSCALE_SECRET_KEY:-$(_readaccountconf_mutable EXOSCALE_SECRET_KEY)}" - if [ -z "$EXOSCALE_API_KEY" ] || [ -z "$EXOSCALE_SECRET_KEY" ]; then - _err "EXOSCALE_API_KEY and EXOSCALE_SECRET_KEY must be set." - return 1 - fi - _saveaccountconf_mutable EXOSCALE_API_KEY "$EXOSCALE_API_KEY" - _saveaccountconf_mutable EXOSCALE_SECRET_KEY "$EXOSCALE_SECRET_KEY" return 0 } -_get_root_domain_id() { +#################### Private functions below ################################## + +_checkAuth() { + EXOSCALE_API_KEY="${EXOSCALE_API_KEY:-$(_readaccountconf_mutable EXOSCALE_API_KEY)}" + EXOSCALE_SECRET_KEY="${EXOSCALE_SECRET_KEY:-$(_readaccountconf_mutable EXOSCALE_SECRET_KEY)}" + + if [ -z "$EXOSCALE_API_KEY" ] || [ -z "$EXOSCALE_SECRET_KEY" ]; then + EXOSCALE_API_KEY="" + EXOSCALE_SECRET_KEY="" + _err "You don't specify Exoscale application key and application secret yet." + _err "Please create you key and try again." + return 1 + fi + + _saveaccountconf_mutable EXOSCALE_API_KEY "$EXOSCALE_API_KEY" + _saveaccountconf_mutable EXOSCALE_SECRET_KEY "$EXOSCALE_SECRET_KEY" + + return 0 +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +# _domain_token=sdjkglgdfewsdfg +_get_root() { + + if ! _exoscale_rest GET "domains"; then + return 1 + fi + domain=$1 - i=1 + i=2 + p=1 while true; do - candidate=$(printf "%s" "$domain" | cut -d . -f "${i}-100") - [ -z "$candidate" ] && return 1 - _debug "Trying root domain candidate: $candidate" - domains=$(_exoscale_rest GET "/dns-domain") - # Extract from dns-domains array - result=$(echo "$domains" | _egrep_o '"dns-domains":\[.*\]' | _egrep_o '\{"id":"[^"]*","created-at":"[^"]*","unicode-name":"[^"]*"\}' | while read -r item; do - name=$(echo "$item" | _egrep_o '"unicode-name":"[^"]*"' | cut -d'"' -f4) - id=$(echo "$item" | _egrep_o '"id":"[^"]*"' | cut -d'"' -f4) - if [ "$name" = "$candidate" ]; then - echo "$id" - break - fi - done) - if [ -n "$result" ]; then - echo "$result" - return 0 + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 fi + + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + _domain_id=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \") + _domain_token=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + if [ "$_domain_token" ] && [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + return 0 + fi + return 1 + fi + p=$i i=$(_math "$i" + 1) done + return 1 } -_get_sub_domain() { - fulldomain=$1 - root_id=$2 - root_info=$(_exoscale_rest GET "/dns-domain/$root_id") - _debug root_info "$root_info" - root_name=$(echo "$root_info" | _egrep_o "\"unicode-name\":\"[^\"]*\"" | cut -d\" -f4) - sub=${fulldomain%%."$root_name"} - - if [ "$sub" = "_acme-challenge" ]; then - echo "" - else - # Remove _acme-challenge. prefix to get the actual subdomain - echo "${sub#_acme-challenge.}" - fi -} - -_find_record_id() { - root_id=$1 - name=$2 - records=$(_exoscale_rest GET "/dns-domain/$root_id/record") - - # Convert search name to lowercase for case-insensitive matching - name_lower=$(echo "$name" | tr '[:upper:]' '[:lower:]') - - echo "$records" | _egrep_o '\{[^}]*"name":"[^"]*"[^}]*\}' | while read -r record; do - record_name=$(echo "$record" | _egrep_o '"name":"[^"]*"' | cut -d'"' -f4) - record_name_lower=$(echo "$record_name" | tr '[:upper:]' '[:lower:]') - if [ "$record_name_lower" = "$name_lower" ]; then - echo "$record" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d'"' -f4 - break - fi - done -} - -_exoscale_sign() { - k=$1 - shift - hex_key=$(printf %b "$k" | _hex_dump | tr -d ' ') - printf %s "$@" | _hmac sha256 "$hex_key" -} - +# returns response _exoscale_rest() { method=$1 - path=$2 - data=$3 - - url="${EXOSCALE_API}${path}" - expiration=$(_math "$(date +%s)" + 300) # 5m from now - - # Build the message with the actual body or empty line - message=$(printf "%s %s\n%s\n\n\n%s" "$method" "/v2$path" "$data" "$expiration") - signature=$(_exoscale_sign "$EXOSCALE_SECRET_KEY" "$message" | _base64) - auth="EXO2-HMAC-SHA256 credential=${EXOSCALE_API_KEY},expires=${expiration},signature=${signature}" - - _debug "API request: $method $url" - _debug "Signed message: [$message]" - _debug "Authorization header: [$auth]" + path="$2" + data="$3" + token="$4" + request_url="$EXOSCALE_API/$path" + _debug "$path" export _H1="Accept: application/json" - export _H2="Authorization: ${auth}" + + if [ "$token" ]; then + export _H2="X-DNS-Domain-Token: $token" + else + export _H2="X-DNS-Token: $EXOSCALE_API_KEY:$EXOSCALE_SECRET_KEY" + fi if [ "$data" ] || [ "$method" = "DELETE" ]; then export _H3="Content-Type: application/json" _debug data "$data" - response="$(_post "$data" "$url" "" "$method")" + response="$(_post "$data" "$request_url" "" "$method")" else - response="$(_get "$url" "" "" "$method")" + response="$(_get "$request_url" "" "" "$method")" fi - # shellcheck disable=SC2181 - if [ "$?" -ne 0 ]; then - _err "error $url" + if [ "$?" != "0" ]; then + _err "error $request_url" return 1 fi _debug2 response "$response" - echo "$response" return 0 } diff --git a/dnsapi/dns_firestorm.sh b/dnsapi/dns_firestorm.sh deleted file mode 100644 index 808c2b89..00000000 --- a/dnsapi/dns_firestorm.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_firestorm_info='Firestorm.ch -Site: firestorm.ch -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_firestorm -Options: - FST_Key Customer ID - FST_Secret API Secret - FST_Url API URL. Optional. Default "https://api.firestorm.ch/acme-dns". -Issues: github.com/acmesh-official/acme.sh/issues/6839 -Author: FireStorm GmbH -' - -FST_Url_DEFAULT="https://api.firestorm.ch/acme-dns" - -######## Public functions ##################### - -# Usage: dns_firestorm_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_firestorm_add() { - fulldomain=$1 - txtvalue=$2 - - FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}" - FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}" - FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}" - - if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then - _err "FST_Key and FST_Secret must be set" - _err "Get your API credentials at https://admin.firestorm.ch" - return 1 - fi - - FST_Url="${FST_Url:-$FST_Url_DEFAULT}" - - _saveaccountconf_mutable FST_Key "$FST_Key" - _saveaccountconf_mutable FST_Secret "$FST_Secret" - if [ "$FST_Url" != "$FST_Url_DEFAULT" ]; then - _saveaccountconf_mutable FST_Url "$FST_Url" - else - _clearaccountconf_mutable FST_Url - fi - - subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//') - - _info "Adding TXT record for $fulldomain" - _debug "Subdomain" "$subdomain" - _debug "TXT value" "$txtvalue" - - body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}" - - response="$(_firestorm_api "update" "$body")" - - if _contains "$response" "$txtvalue"; then - _info "TXT record added successfully" - return 0 - fi - - _err "Failed to add TXT record: $response" - return 1 -} - -# Usage: dns_firestorm_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_firestorm_rm() { - fulldomain=$1 - txtvalue=$2 - - FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}" - FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}" - FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}" - FST_Url="${FST_Url:-$FST_Url_DEFAULT}" - - if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then - _err "FST_Key and FST_Secret must be set" - return 1 - fi - - subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//') - - _info "Removing TXT record for $fulldomain" - - body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}" - - response="$(_firestorm_api "remove" "$body")" - - if _contains "$response" "removed"; then - _info "TXT record removed" - return 0 - fi - - _err "Failed to remove TXT record: $response" - return 1 -} - -#################### Private functions below ################################## - -# Escape special characters for safe JSON string interpolation -_json_safe() { - printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' -} - -_firestorm_api() { - action=$1 - data=$2 - - export _H1="X-Api-User: $FST_Key" - export _H2="X-Api-Key: $FST_Secret" - export _H3="Content-Type: application/json" - - _post "$data" "$FST_Url/$action" "" "POST" -} diff --git a/dnsapi/dns_fornex.sh b/dnsapi/dns_fornex.sh index dcaa2297..91e5491b 100644 --- a/dnsapi/dns_fornex.sh +++ b/dnsapi/dns_fornex.sh @@ -95,7 +95,7 @@ _get_root() { return 1 fi - if ! _rest GET "dns/domain/?q=$h"; then + if ! _rest GET "dns/domain/"; then return 1 fi diff --git a/dnsapi/dns_freedns.sh b/dnsapi/dns_freedns.sh index 8ea86c24..114f30e0 100755 --- a/dnsapi/dns_freedns.sh +++ b/dnsapi/dns_freedns.sh @@ -7,7 +7,7 @@ Options: FREEDNS_User Username FREEDNS_Password Password Issues: github.com/acmesh-official/acme.sh/issues/2305 -Author: David Kerr <@dkerr64> +Author: David Kerr ' ######## Public functions ##################### @@ -305,7 +305,7 @@ _freedns_domain_id() { fi domain_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's//@/g' | tr '@' '\n' | - grep -E "$search_domain|$search_domain\(.*\)" | + grep "$search_domain\|$search_domain(.*)" | sed -n 's/.*\(edit\.php?edit_domain_id=[0-9a-zA-Z]*\).*/\1/p' | cut -d = -f 2)" # The above beauty extracts domain ID from the html page... diff --git a/dnsapi/dns_freemyip.sh b/dnsapi/dns_freemyip.sh index 18d8e7f9..0bad3809 100644 --- a/dnsapi/dns_freemyip.sh +++ b/dnsapi/dns_freemyip.sh @@ -1,12 +1,12 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 dns_freemyip_info='FreeMyIP.com -Site: FreeMyIP.com +Site: freemyip.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_freemyip Options: FREEMYIP_Token API Token -Issues: github.com/acmesh-official/acme.sh/issues/6247 -Author: Recolic Keghart , @Giova96, ExtremeFiretop +Issues: github.com/acmesh-official/acme.sh/issues/{XXXX} +Author: Recolic Keghart , @Giova96 ' FREEMYIP_DNS_API="https://freemyip.com/update?" @@ -68,30 +68,22 @@ dns_freemyip_rm() { return $? } -################ Private functions below ################ +################ Private functions below ################ _get_root() { _fmi_d="$1" - echo "$_fmi_d" | sed 's/.*\.\([^.]*\.[^.]*\.[^.]*\)$/\1/' + echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev } # There is random failure while calling freemyip API too fast. This function automatically retry until success. _freemyip_get_until_ok() { _fmi_url="$1" - _fmi_i=1 - while [ "$_fmi_i" -le 8 ]; do - _debug "HTTP GET freemyip.com API '$_fmi_url', retry $_fmi_i/8..." - _fmi_response="$(_get "$_fmi_url")" - printf '%s\n' "$_fmi_response" >&2 - - if _contains "$_fmi_response" "OK"; then - return 0 - fi - + for i in $(seq 1 8); do + _debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..." + _get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0 _sleep 1 # DO NOT send the request too fast - _fmi_i=$((_fmi_i + 1)) done - _err "Failed to request freemyip API. Server does not say 'OK'" + _err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'" return 1 } @@ -101,16 +93,13 @@ _is_root_domain_published() { _webroot="$(_get_root "$_fmi_d")" _info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet" - _fmi_i=1 - while [ "$_fmi_i" -le 3 ]; do - _debug "'$_webroot' ns lookup, retry $_fmi_i/3..." - + for i in $(seq 1 3); do + _debug "'$_webroot' ns lookup, retry $i/3..." if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then _debug "'$_webroot' already has a TXT record published!" return 0 fi _sleep 10 # Give it some time to propagate the TXT record - _fmi_i=$((_fmi_i + 1)) done return 1 } diff --git a/dnsapi/dns_gandi_livedns.sh b/dnsapi/dns_gandi_livedns.sh index aaef07bf..0516fee9 100644 --- a/dnsapi/dns_gandi_livedns.sh +++ b/dnsapi/dns_gandi_livedns.sh @@ -23,8 +23,6 @@ dns_gandi_livedns_add() { fulldomain=$1 txtvalue=$2 - GANDI_LIVEDNS_KEY="${GANDI_LIVEDNS_KEY:-$(_readaccountconf_mutable GANDI_LIVEDNS_KEY)}" - GANDI_LIVEDNS_TOKEN="${GANDI_LIVEDNS_TOKEN:-$(_readaccountconf_mutable GANDI_LIVEDNS_TOKEN)}" if [ -z "$GANDI_LIVEDNS_KEY" ] && [ -z "$GANDI_LIVEDNS_TOKEN" ]; then _err "No Token or API key (deprecated) specified for Gandi LiveDNS." _err "Create your token or key and export it as GANDI_LIVEDNS_KEY or GANDI_LIVEDNS_TOKEN respectively" @@ -33,11 +31,11 @@ dns_gandi_livedns_add() { # Keep only one secret in configuration if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then - _saveaccountconf_mutable GANDI_LIVEDNS_TOKEN "$GANDI_LIVEDNS_TOKEN" - _clearaccountconf_mutable GANDI_LIVEDNS_KEY + _saveaccountconf GANDI_LIVEDNS_TOKEN "$GANDI_LIVEDNS_TOKEN" + _clearaccountconf GANDI_LIVEDNS_KEY elif [ -n "$GANDI_LIVEDNS_KEY" ]; then - _saveaccountconf_mutable GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY" - _clearaccountconf_mutable GANDI_LIVEDNS_TOKEN + _saveaccountconf GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY" + _clearaccountconf GANDI_LIVEDNS_TOKEN fi _debug "First detect the root zone" diff --git a/dnsapi/dns_gd.sh b/dnsapi/dns_gd.sh index c92bdfa2..ee66ee19 100755 --- a/dnsapi/dns_gd.sh +++ b/dnsapi/dns_gd.sh @@ -69,12 +69,7 @@ dns_gd_add() { return 1 fi - if _contains "$response" "UNKNOWN_DOMAIN"; then - # GoDaddy sometimes returns UNKNOWN_DOMAIN when reading a record back even - # though the PUT above succeeded; skip the local readback check and let - # acme.sh's own DNS propagation check verify the record was published. - _info "GoDaddy API won't allow reading the record back; skipping local verification." - elif ! _contains "$response" "$txtvalue"; then + if ! _contains "$response" "$txtvalue"; then _err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!" return 1 fi @@ -150,8 +145,8 @@ dns_gd_rm() { # _domain=domain.com _get_root() { domain=$1 - i=1 - p=0 + i=2 + p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) if [ -z "$h" ]; then @@ -159,41 +154,17 @@ _get_root() { return 1 fi - # The record name is whatever precedes the candidate zone. Do not assume - # _acme-challenge here: with DNS alias mode it can be any name, and the - # record may even sit at the zone apex (name "@"). - if [ "$p" = "0" ]; then - _probe_sub="@" - else - _probe_sub=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - fi - - # Probe with the records endpoint instead of "GET domains/$h": since - # 2024-05 GoDaddy rejects the domain details call for accounts with - # fewer than 10 domains, while record-level calls keep working. - # https://github.com/acmesh-official/acme.sh/issues/4487 - if ! _gd_rest GET "domains/$h/records/TXT/$_probe_sub"; then - return 1 - fi - if _startswith "$response" '\['; then - _sub_domain="$_probe_sub" - _domain="$h" - return 0 - fi - - # Some accounts get UNKNOWN_DOMAIN when reading records of a valid zone - # even though writes succeed (see issue #6517); fall back to the domain - # details call for them. if ! _gd_rest GET "domains/$h"; then return 1 fi - if _contains "$response" '"domainId"'; then - _sub_domain="$_probe_sub" + + if _contains "$response" '"code":"NOT_FOUND"'; then + _debug "$h not found" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _domain="$h" return 0 fi - - _debug "$h not found" p="$i" i=$(_math "$i" + 1) done diff --git a/dnsapi/dns_glesys.sh b/dnsapi/dns_glesys.sh deleted file mode 100644 index 008abd12..00000000 --- a/dnsapi/dns_glesys.sh +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_glesys_info='Glesys -Site: Glesys.se -Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_glesys -Options: - GLESYS_API_KEY Generated API key. - GLESYS_PROJECT_ID Project ID for the API key (e.g. cl12345). - GLESYS_API API endpoint. Default "https://api.glesys.com/domain". - GLESYS_TTL TXT record TTL. Default 120. -Issues: https://github.com/acmesh-official/acme.sh/issues/7057 -Author: Toni Karppi -' - -GLESYS_API_DEFAULT="https://api.glesys.com/domain" -GLESYS_TTL_DEFAULT="120" - -######## Public functions ##################################################### - -# Usage: -# dns_glesys_add _acme-challenge.www.example.com "txt-value" -dns_glesys_add() { - fulldomain="$1" - txtvalue="$2" - - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - _glesys_init || return 1 - - if ! _glesys_get_root "$fulldomain"; then - _err "Could not find root zone for $fulldomain" - return 1 - fi - - _debug _domain "$_domain" - _debug _sub_domain "$_sub_domain" - - host_value="${_sub_domain:-@}" - _debug _host_value "$host_value" - - data="{\"domainname\":\"$_domain\",\"host\":\"$host_value\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":\"$GLESYS_TTL\"}" - - _debug2 data "$data" - - if ! _glesys_rest POST "/addrecord" "$data"; then - _err "Failed to send HTTP request to add TXT record" - return 1 - fi - - response_code=$( - printf "%s" "$response" | - tr -d '\r\n\t ' | - _egrep_o '"code":"?[0-9]+' | - _egrep_o '[0-9]+$' - ) - - _debug response_code "$response_code" - - if [ "$response_code" != "200" ]; then - _err "GleSYS API responded with an unexpected status when attempting to add TXT record" - _debug2 "API response" "$response" - return 1 - fi - - _info "TXT record added" - - return 0 -} - -# Usage: -# dns_glesys_rm _acme-challenge.www.example.com "txt-value" -dns_glesys_rm() { - fulldomain="$1" - txtvalue="$2" - - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - _glesys_init || return 1 - - if ! _glesys_get_root "$fulldomain"; then - _err "Could not find root zone for $fulldomain" - return 1 - fi - - if ! _glesys_find_record_id "$txtvalue"; then - _info "TXT record not present, skip removal" - return 0 - fi - - _debug _record_id "$_record_id" - - if ! _glesys_rest POST "/deleterecord" "{\"recordid\":$_record_id}"; then - _err "Failed to send HTTP request to remove TXT record" - return 1 - fi - - response_code=$( - printf "%s" "$response" | - tr -d '\r\n\t ' | - _egrep_o '"code":"?[0-9]+' | - _egrep_o '[0-9]+$' - ) - - _debug response_code "$response_code" - - if [ "$response_code" != "200" ]; then - _err "GleSYS API responded with unexpected status when attempting to remove TXT record" - _debug2 "API response" "$response" - return 1 - fi - - _info "TXT record removed" - - return 0 -} - -######## Private functions #################################################### - -_glesys_find_record_id() { - txtvalue="$1" - - _debug txtvalue "$txtvalue" - - if [ -z "$txtvalue" ]; then - return 1 - fi - - _record_id="" - - _debug "Looking for TXT record with value" "$txtvalue" - - if ! _glesys_rest GET "/listrecords?domainname=$_domain"; then - _err "Failed to list DNS records" - return 1 - fi - - records="$( - printf "%s" "$response" | - tr -d '\r\n\t ' | - sed 's/},{/}\ -{/g' - )" - - _debug2 records "$records" - - expected_data="\"data\":\"$txtvalue\"" - - _record_id="$( - printf "%s\n" "$records" | - while IFS= read -r record; do - printf "%s" "$record" | grep -q '"type":"TXT"' || continue - printf "%s" "$record" | grep -Fq "$expected_data" || continue - - printf "%s" "$record" | - grep -E -o '"recordid":"?[0-9]+' | - grep -E -o '[0-9]+$' - - break - done - )" - - _debug _record_id "$_record_id" - - if [ -z "$_record_id" ]; then - return 1 - fi - - return 0 -} - -# Finds: -# _domain example.com -# _sub_domain _acme-challenge.www -_glesys_get_root() { - domain="$1" - i=1 - - while true; do - h="$(printf "%s" "$domain" | cut -d . -f "$i"-100)" - - if [ -z "$h" ]; then - return 1 - fi - - if _glesys_rest GET "/listrecords?domainname=$h"; then - response_code=$( - printf "%s" "$response" | - tr -d '\r\n\t ' | - _egrep_o '"code":"?[0-9]+' | - _egrep_o '[0-9]+$' - ) - - _debug response_code "$response_code" - - if [ "$response_code" = "200" ]; then - cut_len="$((${#domain} - ${#h} - 1))" - _domain="$h" - _sub_domain="$(printf "%s" "$domain" | cut -c "1-$cut_len")" - return 0 - fi - fi - - i="$((i + 1))" - done -} - -_glesys_init() { - [ -z "$GLESYS_API" ] && GLESYS_API="$GLESYS_API_DEFAULT" - [ -z "$GLESYS_TTL" ] && GLESYS_TTL="$GLESYS_TTL_DEFAULT" - - _debug GLESYS_API "$GLESYS_API" - _debug GLESYS_TTL "$GLESYS_TTL" - - GLESYS_API_KEY="${GLESYS_API_KEY:-$(_readaccountconf_mutable GLESYS_API_KEY)}" - GLESYS_PROJECT_ID="${GLESYS_PROJECT_ID:-$(_readaccountconf_mutable GLESYS_PROJECT_ID)}" - - if [ -z "$GLESYS_API_KEY" ] || [ -z "$GLESYS_PROJECT_ID" ]; then - _err "GLESYS_API_KEY and GLESYS_PROJECT_ID must be set for this provider" - return 1 - fi - - _secure_debug GLESYS_API_KEY "$GLESYS_API_KEY" - _secure_debug GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID" - - _glesys_basic_auth="$(printf "%s:%s" "$GLESYS_PROJECT_ID" "$GLESYS_API_KEY" | _base64)" - _secure_debug2 _glesys_basic_auth "$_glesys_basic_auth" - - _saveaccountconf_mutable GLESYS_API_KEY "$GLESYS_API_KEY" - _saveaccountconf_mutable GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID" - - return 0 -} - -_glesys_rest() { - method="$1" - path="$2" - data="$3" - - export _H1="Authorization: Basic $_glesys_basic_auth" - export _H2="Content-Type: application/json" - export _H3="Accept: application/json" - - url="$GLESYS_API$path" - _debug "$method $url" - - if [ "$method" = "GET" ]; then - response="$(_get "$url")" - else - response="$(_post "$data" "$url" "" "$method")" - fi - - ret="$?" - _debug2 response "$response" - _debug ret "$ret" - - if [ "$ret" != "0" ]; then - return 1 - fi - - return 0 -} diff --git a/dnsapi/dns_gname.sh b/dnsapi/dns_gname.sh deleted file mode 100644 index 886b3dc5..00000000 --- a/dnsapi/dns_gname.sh +++ /dev/null @@ -1,303 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_gname_info='GNAME -Site: www.gname.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gname -Options: - GNAME_APPID Your APPID - GNAME_APPKEY Your APPKEY - GNAME_TTL DNS resolution record TTL value, default 120. -Issues: github.com/acmesh-official/acme.sh/issues/6874 -Author: GNDevProd -' - -GNAME_TLD_Api="https://www.gname.com/request/tlds?lx=all" -GNAME_Api="https://api.gname.com" -GNAME_TLDS_CACHE="" - -######## Public functions ##################### - -#Usage: add _acme-challenge.www.domain.com "T1rxqRBosdIK90xWCG3KLZNf6q_0HG9i01zxXp5CAS3" -dns_gname_add() { - fulldomain=$1 - txtvalue=$(printf "%s" "$2" | _url_encode) - #Compatible with gname API RFC 1738 standard URL encoding - txtvalue=$(printf '%s' "$txtvalue" | sed 's/%20/+/g') - - GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}" - GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}" - GNAME_TTL="${GNAME_TTL:-$(_readaccountconf_mutable GNAME_TTL)}" - GNAME_TTL="${GNAME_TTL:-120}" - - if [ -z "$GNAME_APPID" ] || [ -z "$GNAME_APPKEY" ]; then - GNAME_APPID="" - GNAME_APPKEY="" - _err "You have not configured the APPID and APPKEY for the GNAME API." - _err "You can get yours from here https://www.gname.com/domain/api." - return 1 - fi - - _saveaccountconf_mutable GNAME_APPID "$GNAME_APPID" - _saveaccountconf_mutable GNAME_APPKEY "$GNAME_APPKEY" - _saveaccountconf_mutable GNAME_TTL "$GNAME_TTL" - - if ! _extract_domain "$fulldomain"; then - _err "Failed to extract domain. Please check your network or API response." - return 1 - fi - - gntime=$(date +%s) - - #If the hostname is empty, you need to replace it with @. - final_hostname=$(printf "%s" "${ext_hostname:-@}" | _url_encode) - - # Parameters need to be sorted by key - body="appid=$GNAME_APPID&exist=1&gntime=$gntime&jlz=$txtvalue&lang=us&lx=TXT&mx=0&ttl=$GNAME_TTL&xl=0&ym=$ext_domain&zj=$final_hostname" - - _info "Adding TXT record for $ext_domain, host: $final_hostname" - - if _post_to_api "/api/resolution/add" "$body"; then - _info "Successfully added DNS record." - return 0 - else - _err "Failed to add DNS record via Gname API." - return 1 - fi -} - -#Usage: remove _acme-challenge.www.domain.com "T1rxqRBosdIK90xWCG3KLZNf6q_0HG9i01zxXp5CASc" -dns_gname_rm() { - fulldomain=$1 - txtvalue=$2 - - GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}" - GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}" - - if [ -z "$GNAME_APPID" ] || [ -z "$GNAME_APPKEY" ]; then - GNAME_APPID="" - GNAME_APPKEY="" - _err "You have not configured the APPID and APPKEY for the GNAME API." - _err "You can get yours from here https://www.gname.com/domain/api." - return 1 - fi - - _saveaccountconf_mutable GNAME_APPID "$GNAME_APPID" - _saveaccountconf_mutable GNAME_APPKEY "$GNAME_APPKEY" - - if ! _extract_domain "$fulldomain"; then - _err "Failed to extract domain. Please check your network or API response." - return 1 - fi - - final_hostname="${ext_hostname:-@}" - - _debug "Query DNS record ID $ext_domain $final_hostname $txtvalue" - - if ! record_id=$(_get_record_id "$ext_domain" "$final_hostname" "$txtvalue"); then - _err "Error occurred during record lookup. Skipping deletion to avoid errors." - return 1 - fi - - if [ -z "$record_id" ]; then - _info "DNS record not found, skip removing." - return 0 - fi - - _debug "DNS record ID:$record_id" - gntime=$(date +%s) - body="appid=$GNAME_APPID&gntime=$gntime&jxid=$record_id&lang=us&ym=$ext_domain" - - if ! _post_to_api "/api/resolution/delete" "$body"; then - _err "DNS record deletion failed" - return 1 - fi - - _info "DNS record deletion successful" - return 0 -} - -# Find the DNS record ID by hostname, record type, and record value. -_get_record_id() { - target_ym="$1" - target_zjt="$2" - target_jxz="$3" - target_lx="TXT" - - GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}" - GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}" - gntime=$(date +%s) - body="appid=$GNAME_APPID&gntime=$gntime&limit=1000&lx=$target_lx&page=1&ym=$target_ym" - - if ! _post_to_api "/api/resolution/list" "$body"; then - _err "Query and parsing records failed" - return 1 - fi - - clean_response=$(echo "$post_response" | tr -d '\r') - records=$(echo "$clean_response" | sed 's/.*"data":\[//; s/\],"count".*//; s/},/}\n/g' | grep "^{") - matched_rows=$(echo "$records" | grep -Fi "\"zjt\":\"$target_zjt\"") - - if [ -z "$matched_rows" ]; then - _debug "No records found for host: $target_zjt" - return 0 - fi - - exact_row=$(echo "$matched_rows" | grep -F "\"jxz\":\"$target_jxz\"" | _head_n 1) - dns_record_id="" - if [ -n "$exact_row" ]; then - dns_record_id=$(echo "$exact_row" | _egrep_o "\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d '"') - fi - - if [ -n "$dns_record_id" ]; then - _debug "Successfully found exact record ID: $dns_record_id" - printf "%s" "$dns_record_id" - return 0 - fi - - _debug "Can not find exact DNS record match for: $target_zjt" - return 0 -} - -# Request GNAME API,post_response: Response content -_post_to_api() { - uri=$1 - body=$2 - url="$GNAME_Api$uri" - gntoken=$(_gntoken "$body") - body="$body&gntoken=$gntoken" - post_response="$(_post "$body" "$url" "" "POST" "application/x-www-form-urlencoded")" - - http_err_code=$? - if [ "$http_err_code" != "0" ]; then - _err "POST API $url request failed:$http_err_code" - return 1 - fi - - normalized_response="$(echo "$post_response" | _normalizeJson)" - if [ -z "$normalized_response" ]; then - _err "Failed to normalize JSON response for [$uri]" - return 1 - fi - - ret_code=$(echo "$normalized_response" | sed 's/.*"code":\([-0-9]*\).*/\1/') - - if [ "$ret_code" = "1" ]; then - return 0 - fi - - if [ "$uri" = "/api/resolution/add" ]; then - if _contains "$normalized_response" "the same host records and record values"; then - _info "DNS record already exists, treat as success." - return 0 - fi - fi - - ret_msg=$(echo "$normalized_response" | sed 's/.*"msg":"\([^"]*\)".*/\1/') - _err "POST API $url error: [$ret_code] $ret_msg" - _debug "Full response: $normalized_response" - return 1 -} - -# Split the complete domain into a host and a main domain. -# example, www.gname.com can be split into ext_hostname=www,ext_domain=gname.com -_extract_domain() { - - host="$1" - - # Prioritize reading from the cache and reduce network caching - if [ -z "$GNAME_TLDS_CACHE" ]; then - GNAME_TLDS_CACHE=$(_get_suffixes_json) - fi - - if [ -z "$GNAME_TLDS_CACHE" ]; then - _err "The list of domain suffixes is empty after retrieval; cannot extract domain" - return 1 - fi - - main_part=$(echo "$GNAME_TLDS_CACHE" | sed 's/.*"main":\[\([^]]*\)\].*/\1/' | tr -d '"' | tr ',' ' ') - sub_part=$(echo "$GNAME_TLDS_CACHE" | sed 's/.*"sub":\[\([^]]*\)\].*/\1/' | tr -d '"' | tr ',' ' ') - suffix_list=$(echo "$main_part $sub_part" | tr -s ' ' | sed 's/^[ ]//;s/[ ]$//') - - dot_count=$(echo "$host" | _egrep_o "\." | wc -l) - - if [ "$dot_count" -eq 0 ]; then - _err "Invalid domain format: $host (missing dot)" - return 1 - fi - - if [ "$dot_count" -eq 1 ]; then - ext_hostname="" - ext_domain="$host" - - elif [ "$dot_count" -gt 1 ]; then - matched_suffix="" - for suffix in $suffix_list; do - case "$host" in - *".$suffix") - if [ -z "$matched_suffix" ] || [ "${#suffix}" -gt "${#matched_suffix}" ]; then - matched_suffix="$suffix" - fi - ;; - esac - done - - if [ -n "$matched_suffix" ]; then - prefix="${host%."$matched_suffix"}" - main_name="${prefix##*.}" - ext_domain="$main_name.$matched_suffix" - else - _tld="${host##*.}" - _tmp="${host%.*}" - _main="${_tmp##*.}" - ext_domain="$_main.$_tld" - fi - - if [ "$host" = "$ext_domain" ]; then - ext_hostname="" - else - ext_hostname="${host%."$ext_domain"}" - fi - - fi - _debug "ext_hostname:$ext_hostname" - _debug "ext_domain:$ext_domain" - return 0 -} - -# Obtain the list of domain suffixes via API -_get_suffixes_json() { - _debug "GET request URL: $GNAME_TLD_Api Retrieves a list of domain suffixes." - - if ! response="$(_get "$GNAME_TLD_Api")"; then - _err "Failed to retrieve list of domain suffixes" - return 1 - fi - - if [ -z "$response" ]; then - _err "The list of domain suffixes is empty" - return 1 - fi - - normalized_response="$(echo "$response" | _normalizeJson)" - if [ -z "$normalized_response" ]; then - _err "Failed to normalize JSON response for domain suffix list" - return 1 - fi - - if ! _contains "$normalized_response" "\"code\":1"; then - _err "Failed to retrieve list of domain name suffixes; code is not 1" - return 1 - fi - - echo "$normalized_response" - return 0 -} - -# Generate API authentication signature -_gntoken() { - data_to_sign="$1" - full_data="${data_to_sign}${GNAME_APPKEY}" - hash=$(printf "%s" "$full_data" | _digest md5 hex | tr -d ' ') - hash_upper=$(echo "$hash" | _upper_case) - printf "%s" "$hash_upper" -} diff --git a/dnsapi/dns_he_ddns.sh b/dnsapi/dns_he_ddns.sh index 1fe9a7fd..cd7d1ec2 100644 --- a/dnsapi/dns_he_ddns.sh +++ b/dnsapi/dns_he_ddns.sh @@ -5,7 +5,6 @@ Site: dns.he.net Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_he_ddns Options: HE_DDNS_KEY The DDNS key -Issues: https://github.com/acmesh-official/acme.sh/issues/5238 Author: Markku Leiniö ' diff --git a/dnsapi/dns_hestiacp.sh b/dnsapi/dns_hestiacp.sh deleted file mode 100644 index 13ee6caf..00000000 --- a/dnsapi/dns_hestiacp.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_hestiacp_info='HestiaCP Server API -Site: hestiacp.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hestiacp -Options: - HESTIA_HOST Panel URL. E.g. "https://panel.example.com:8083" - HESTIA_ACCESS API access key - HESTIA_SECRET API secret key - HESTIA_USER Username owning the DNS zones. Default "admin". Optional. -Issues: github.com/acmesh-official/acme.sh/issues/6251 -Author: Radu Malica -' - -######## Public functions ##################### - -# Usage: dns_hestiacp_add fulldomain txtvalue -dns_hestiacp_add() { - fulldomain=$1 - txtvalue=$2 - - if ! _hestia_init; then - return 1 - fi - - _debug "Detecting the root zone for $fulldomain" - if ! _hestia_get_root "$fulldomain"; then - _err "Cannot find a DNS zone for $fulldomain under user $HESTIA_USER" - return 1 - fi - _debug _hestia_domain "$_hestia_domain" - _debug _hestia_sub "$_hestia_sub" - - # _hestia_get_root left the zone record listing in _hestia_response - if _hestia_find_records "$_hestia_sub" "TXT" | grep -F -- "$txtvalue" >/dev/null; then - _info "The TXT record already exists, skipping" - return 0 - fi - - _info "Adding TXT record for $fulldomain" - if ! _hestia_rest "v-add-dns-record" "$HESTIA_USER" "$_hestia_domain" "$_hestia_sub" "TXT" "$txtvalue" "" "" "yes" "600"; then - _err "Error adding TXT record: $_hestia_response" - return 1 - fi - _info "TXT record added successfully" - return 0 -} - -# Usage: dns_hestiacp_rm fulldomain txtvalue -dns_hestiacp_rm() { - fulldomain=$1 - txtvalue=$2 - - if ! _hestia_init; then - return 1 - fi - - _debug "Detecting the root zone for $fulldomain" - if ! _hestia_get_root "$fulldomain"; then - _err "Cannot find a DNS zone for $fulldomain under user $HESTIA_USER" - return 1 - fi - _debug _hestia_domain "$_hestia_domain" - _debug _hestia_sub "$_hestia_sub" - - _hestia_removed=0 - _hestia_failed=0 - while IFS='|' read -r _hestia_id _hestia_value || [ -n "$_hestia_id" ]; do - if [ -z "$_hestia_id" ]; then - continue - fi - if ! _contains "$_hestia_value" "$txtvalue"; then - continue - fi - _info "Deleting TXT record $_hestia_id" - if ! _hestia_rest "v-delete-dns-record" "$HESTIA_USER" "$_hestia_domain" "$_hestia_id" "yes"; then - _err "Error deleting TXT record $_hestia_id: $_hestia_response" - _hestia_failed=$(_math "$_hestia_failed" + 1) - continue - fi - _hestia_removed=$(_math "$_hestia_removed" + 1) - done <"${HTTP_HEADER}" - - if [ "${method}" = "GET" ]; then - response="$(_get "${url}")" - else - if [ -z "${data}" ]; then - data="{}" - fi - response="$(_post "${data}" "${url}" "" "${method}" "application/json")" - fi - ret="${?}" - - _hetznercloud_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n') - - if [ "${ret}" != "0" ]; then - return 1 - fi - - if [ "${_hetznercloud_last_http_code}" = "429" ] && [ "${retried}" != "retried" ]; then - retry_after=$(grep -i "^Retry-After" "${HTTP_HEADER}" | _tail_n 1 | cut -d : -f 2 | tr -d ' \r') - if [ -z "${retry_after}" ]; then - retry_after=1 - fi - _info "Hetzner Cloud DNS API rate limit hit; retrying in ${retry_after} seconds." - _sleep "${retry_after}" - if ! _hetznercloud_api "${method}" "${ep}" "${data}" "retried"; then - return 1 - fi - return 0 - fi - - return 0 -} - -_hetznercloud_handle_action_response() { - context="${1}" - if [ -z "${response}" ]; then - return 0 - fi - - normalized=$(printf "%s" "${response}" | _normalizeJson) - - failed_message="" - if failed_message=$(_hetznercloud_extract_failed_action_message "${normalized}"); then - if [ -n "${failed_message}" ]; then - _err "Hetzner Cloud DNS ${context} failed: ${failed_message}" - else - _err "Hetzner Cloud DNS ${context} failed." - fi - return 1 - fi - - action_ids="" - if action_ids=$(_hetznercloud_extract_action_ids "${normalized}"); then - for action_id in ${action_ids}; do - if [ -z "${action_id}" ]; then - continue - fi - if ! _hetznercloud_wait_for_action "${action_id}" "${context}"; then - return 1 - fi - done - fi - - return 0 -} - -_hetznercloud_extract_failed_action_message() { - normalized="${1}" - failed_section=$(printf "%s" "${normalized}" | _egrep_o '"failed_actions":\[[^]]*\]') - if [ -z "${failed_section}" ]; then - return 1 - fi - if _contains "${failed_section}" '"failed_actions":[]'; then - return 1 - fi - message=$(printf "%s" "${failed_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') - if [ -n "${message}" ]; then - printf "%s" "${message}" - else - printf "%s" "${failed_section}" - fi - return 0 -} - -_hetznercloud_extract_action_ids() { - normalized="${1}" - actions_section=$(printf "%s" "${normalized}" | _egrep_o '"actions":\[[^]]*\]') - if [ -z "${actions_section}" ]; then - return 1 - fi - action_ids=$(printf "%s" "${actions_section}" | _egrep_o '"id":[0-9]*' | cut -d : -f 2 | tr -d '"' | tr '\n' ' ') - action_ids=$(printf "%s" "${action_ids}" | tr -s ' ') - action_ids=$(printf "%s" "${action_ids}" | sed 's/^ //;s/ $//') - if [ -z "${action_ids}" ]; then - return 1 - fi - printf "%s" "${action_ids}" - return 0 -} - -_hetznercloud_wait_for_action() { - action_id="${1}" - context="${2}" - attempts="0" - - while true; do - if ! _hetznercloud_api GET "/actions/${action_id}"; then - return 1 - fi - if [ "${_hetznercloud_last_http_code}" != "200" ]; then - _hetznercloud_log_http_error "Hetzner Cloud DNS action ${action_id} query failed" "${_hetznercloud_last_http_code}" - return 1 - fi - - normalized=$(printf "%s" "${response}" | _normalizeJson) - action_status=$(_hetznercloud_action_status_from_normalized "${normalized}") - - if [ -z "${action_status}" ]; then - _err "Hetzner Cloud DNS ${context} action ${action_id} returned no status." - return 1 - fi - - if [ "${action_status}" = "success" ]; then - return 0 - fi - - if [ "${action_status}" = "error" ]; then - if action_error=$(_hetznercloud_action_error_from_normalized "${normalized}"); then - _err "Hetzner Cloud DNS ${context} action ${action_id} failed: ${action_error}" - else - _err "Hetzner Cloud DNS ${context} action ${action_id} failed." - fi - return 1 - fi - - attempts=$(_math "${attempts}" + 1) - if [ "${attempts}" -ge "${HETZNER_MAX_ATTEMPTS}" ]; then - _err "Hetzner Cloud DNS ${context} action ${action_id} did not complete after ${HETZNER_MAX_ATTEMPTS} attempts." - return 1 - fi - - _sleep 1 - done -} - -_hetznercloud_action_status_from_normalized() { - normalized="${1}" - status=$(printf "%s" "${normalized}" | _egrep_o '"status":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') - printf "%s" "${status}" -} - -_hetznercloud_action_error_from_normalized() { - normalized="${1}" - error_section=$(printf "%s" "${normalized}" | _egrep_o '"error":{[^}]*}') - if [ -z "${error_section}" ]; then - return 1 - fi - message=$(printf "%s" "${error_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') - if [ -n "${message}" ]; then - printf "%s" "${message}" - return 0 - fi - code=$(printf "%s" "${error_section}" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') - if [ -n "${code}" ]; then - printf "%s" "${code}" - return 0 - fi - return 1 -} diff --git a/dnsapi/dns_hostingde.sh b/dnsapi/dns_hostingde.sh index ed675b42..41ccab2b 100644 --- a/dnsapi/dns_hostingde.sh +++ b/dnsapi/dns_hostingde.sh @@ -40,11 +40,6 @@ _hostingde_apiKey() { return 1 fi - # The endpoint is the base URL only; the api path is appended below. - # hosting.de's own docs show the full api URL, so strip it if pasted in. - # https://github.com/acmesh-official/acme.sh/issues/6896 - HOSTINGDE_ENDPOINT="$(echo "$HOSTINGDE_ENDPOINT" | sed 's|/api/dns/v1/json||; s|/*$||')" - _saveaccountconf_mutable HOSTINGDE_APIKEY "$HOSTINGDE_APIKEY" _saveaccountconf_mutable HOSTINGDE_ENDPOINT "$HOSTINGDE_ENDPOINT" } diff --git a/dnsapi/dns_hostinger.sh b/dnsapi/dns_hostinger.sh deleted file mode 100755 index 665c65da..00000000 --- a/dnsapi/dns_hostinger.sh +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_hostinger_info='Hostinger -Site: Hostinger.com -Domains: hostinger.nl -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hostinger -Options: - HOSTINGER_Token API Key -Issues: https://github.com/acmesh-official/acme.sh/issues/6831 -Author: Sasha Reid -' - -HOSTINGER_Api="https://developers.hostinger.com/api/dns/v1/zones" - -######## Public functions ##################### - -#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_hostinger_add() { - fulldomain=$1 - txtvalue=$2 - - HOSTINGER_Token="${HOSTINGER_Token:-$(_readaccountconf_mutable HOSTINGER_Token)}" - - if [ -z "$HOSTINGER_Token" ]; then - HOSTINGER_Token="" - _err "You didn't specify a Hostinger API Key yet." - _err "Please read the documentation for the Hostinger API authentication at https://developers.hostinger.com/#description/authentication" - return 1 - fi - _saveaccountconf_mutable HOSTINGER_Token "$HOSTINGER_Token" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - return 1 - fi - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _debug "Getting existing records" - _hostinger_rest GET "${_domain}" - - if [ -z "$response" ]; then - _err "Error" - return 1 - fi - - # For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so - # we can not use updating anymore. - # count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) - # _debug count "$count" - # if [ "$count" = "0" ]; then - _info "Adding record" - if _hostinger_rest PUT "$_domain" "{\"zone\":[{\"name\": \"$_sub_domain\",\"records\": [{\"content\":\"$txtvalue\"}],\"type\":\"TXT\",\"ttl\":\"120\"}],\"overwrite\":false}"; then - if _contains "$response" "Request accepted"; then - _info "Added, OK" - return 0 - elif _contains "$response" "DNS resource record is not valid or conflicts with another resource record" || - _contains "$response" 'DNS:4008'; then - _info "Already exists, OK" - return 0 - else - _err "Add txt record error." - return 1 - fi - fi - _err "Add txt record error." - return 1 - -} - -#fulldomain txtvalue -dns_hostinger_rm() { - fulldomain=$1 - txtvalue=$2 - - HOSTINGER_Token="${HOSTINGER_Token:-$(_readaccountconf_mutable HOSTINGER_Token)}" - - if [ -z "$HOSTINGER_Token" ]; then - HOSTINGER_Token="" - _err "You didn't specify a Hostinger API Key yet." - _err "Please read the documentation for the Hostinger API authentication at https://developers.hostinger.com/#description/authentication" - return 1 - fi - _saveaccountconf_mutable HOSTINGER_Token "$HOSTINGER_Token" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - return 1 - fi - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _debug "Getting existing records" - _hostinger_rest GET "${_domain}" - - if [ -z "$response" ]; then - _err "Error" - return 1 - fi - - if _contains "$response" "\"name\":\"$_sub_domain\""; then - # Match the record, and make certain it is a TXT record for the domain not another type. Then remove our target record from the list - remaining_records=$(echo "$response" | _normalizeJson | _egrep_o '{"name":"'"$_sub_domain"'","records":\[[^]]+\],"ttl":[0-9]+,"type":"TXT"\}' | _egrep_o "\[.*\]" | sed -E 's#\{"content":"\\"'"$txtvalue"'\\"","is_disabled":false\},?##g') - if [ "$remaining_records" != "[]" ]; then - remaining_json=$(echo "$remaining_records" | _egrep_o '"content":"\\"[^}]+\\""' | sed -E 's/^(.*)$/{\1},/g' | tr -d '\n' | sed 's/,$//') - # We need to set the remaining records back to Hostinger, as we can't partially delete - _info "Removing $txtvalue from $_sub_domain by setting records to ${remaining_json}" - if _hostinger_rest PUT "$_domain" "{\"zone\":[{\"name\": \"$_sub_domain\",\"records\": [${remaining_json}],\"type\":\"TXT\",\"ttl\":\"120\"}],\"overwrite\":true}"; then - if _contains "$response" "Request accepted"; then - _info "Updated remaining records, OK" - return 0 - elif _contains "$response" "DNS resource record is not valid or conflicts with another resource record" || - _contains "$response" 'DNS:4008'; then - _info "Already exists, OK" - return 0 - else - _err "Add txt record error." - return 1 - fi - fi - # Otherwise delete the TXT record that matches the subdomain - else - if ! _hostinger_rest DELETE "$_domain" "{\"filters\":[{\"name\":\"$_sub_domain\",\"type\":\"TXT\"}]}"; then - _err "Delete record error." - return 1 - fi - fi - echo "$response" | grep "Request accepted" >/dev/null - else - _info "Don't need to remove." - fi - -} - -#################### Private functions below ################################## -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com -_get_root() { - domain=$1 - i=1 - p=1 - - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug h "$h" - if [ -z "$h" ]; then - #not valid - return 1 - fi - - _hostinger_rest GET "$h" - if _contains "$response" "records"; then - if [ "$response" = "[]" ]; then - _debug "Valid subdomains are not the root" - else - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - return 0 - fi - fi - - p=$i - i=$(_math "$i" + 1) - done - return 1 -} - -_hostinger_rest() { - m=$1 - ep="$2" - data="$3" - _debug "$ep" - - token_trimmed=$(echo "$HOSTINGER_Token" | tr -d '"') - - export _H1="Content-Type: application/json" - export _H2="Authorization: Bearer $token_trimmed" - - if [ "$m" != "GET" ]; then - _debug data "$data" - response="$(_post "$data" "$HOSTINGER_Api/$ep" "" "$m")" - else - response="$(_get "$HOSTINGER_Api/$ep")" - fi - - if [ "$?" != "0" ]; then - _err "error $ep" - return 1 - fi - _debug2 response "$response" - return 0 -} diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh deleted file mode 100644 index a3d9174a..00000000 --- a/dnsapi/dns_hostup.sh +++ /dev/null @@ -1,577 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034,SC2154 - -dns_hostup_info='HostUp DNS -Site: hostup.se -Docs: https://developer.hostup.se/ -Options: - HOSTUP_API_KEY Required. HostUp API key with read:dns + write:dns + read:domains scopes. - HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api/v2). - HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds). - HOSTUP_ZONE_ID Optional. Force a specific v2 zone ID (zone_...) and skip auto-detection. -Author: HostUp (https://cloud.hostup.se/contact/en) -' - -HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api/v2" -HOSTUP_DEFAULT_TTL=60 - -# Public: add TXT record -# Usage: dns_hostup_add _acme-challenge.example.com "txt-value" -dns_hostup_add() { - fulldomain="$1" - txtvalue="$2" - hostup_add_txtvalue="$2" - - _info "Using HostUp DNS API" - - if ! _hostup_init; then - return 1 - fi - - if ! _hostup_detect_zone "$fulldomain"; then - _err "Unable to determine HostUp zone for $fulldomain" - return 1 - fi - - record_name="$(_hostup_record_name "$fulldomain" "$HOSTUP_ZONE_DOMAIN")" - record_name="$(_hostup_sanitize_name "$record_name")" - hostup_add_record_value="$(_hostup_json_escape "$hostup_add_txtvalue")" - - raw_ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}" - ttl="$(_hostup_normalize_ttl "$raw_ttl")" - if [ -z "$ttl" ]; then - _err "HOSTUP_TTL must be a whole number between 60 and 86400 seconds." - return 1 - fi - if [ -n "$HOSTUP_TTL" ]; then - HOSTUP_TTL="$ttl" - _saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL" - fi - - _debug "zone_id" "$HOSTUP_ZONE_ID" - _debug "zone_domain" "$HOSTUP_ZONE_DOMAIN" - _debug "record_name" "$record_name" - _debug "ttl" "$ttl" - - record_name_fqdn="$(_hostup_fqdn "$fulldomain")" - if _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$hostup_add_txtvalue"; then - _info "TXT record already exists for $fulldomain" - return 0 - fi - - request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$hostup_add_record_value\",\"ttl\":$ttl}" - - if ! _hostup_rest "POST" "/dns-zones/$HOSTUP_ZONE_ID/records" "$request_body"; then - return 1 - fi - - _info "Added TXT record for $fulldomain" - return 0 -} - -# Public: remove TXT record -# Usage: dns_hostup_rm _acme-challenge.example.com "txt-value" -dns_hostup_rm() { - fulldomain="$1" - txtvalue="$2" - - _info "Using HostUp DNS API" - - if ! _hostup_init; then - return 1 - fi - - if ! _hostup_detect_zone "$fulldomain"; then - _err "Unable to determine HostUp zone for $fulldomain" - return 1 - fi - - record_name_fqdn="$(_hostup_fqdn "$fulldomain")" - record_value="$txtvalue" - - if ! _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$record_value"; then - _info "TXT record not found for $record_name_fqdn. Skipping removal." - _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value" - return 0 - fi - - _debug "Deleting record" "$HOSTUP_RECORD_ID" - - if ! _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$HOSTUP_RECORD_ID"; then - return 1 - fi - - _info "Deleted TXT record $HOSTUP_RECORD_ID" - _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value" - HOSTUP_ZONE_ID="" - return 0 -} - -########################## -# Private helper methods # -########################## - -_hostup_init() { - HOSTUP_API_KEY="${HOSTUP_API_KEY:-$(_readaccountconf_mutable HOSTUP_API_KEY)}" - HOSTUP_API_BASE="${HOSTUP_API_BASE:-$(_readaccountconf_mutable HOSTUP_API_BASE)}" - HOSTUP_TTL="${HOSTUP_TTL:-$(_readaccountconf_mutable HOSTUP_TTL)}" - HOSTUP_ZONE_ID="${HOSTUP_ZONE_ID:-$(_readaccountconf_mutable HOSTUP_ZONE_ID)}" - - if [ -z "$HOSTUP_API_BASE" ]; then - HOSTUP_API_BASE="$HOSTUP_API_BASE_DEFAULT" - fi - HOSTUP_API_BASE="$(_hostup_normalize_api_base "$HOSTUP_API_BASE")" - - if [ -z "$HOSTUP_API_KEY" ]; then - HOSTUP_API_KEY="" - _err "HOSTUP_API_KEY is not set." - _err "Please export your HostUp API key with read:dns, write:dns, and read:domains scopes." - return 1 - fi - - _saveaccountconf_mutable HOSTUP_API_KEY "$HOSTUP_API_KEY" - _saveaccountconf_mutable HOSTUP_API_BASE "$HOSTUP_API_BASE" - - if [ -n "$HOSTUP_ZONE_ID" ]; then - _saveaccountconf_mutable HOSTUP_ZONE_ID "$HOSTUP_ZONE_ID" - fi - - return 0 -} - -_hostup_normalize_api_base() { - api_base="${1%/}" - - case "$api_base" in - */api/v2) - printf "%s" "$api_base" - ;; - */api) - printf "%s/v2" "$api_base" - ;; - *) - printf "%s" "$api_base" - ;; - esac -} - -_hostup_normalize_ttl() { - ttl_value="$1" - - case "$ttl_value" in - "" | *[!0-9]*) - return 1 - ;; - esac - - while [ "${ttl_value#0}" != "$ttl_value" ]; do - ttl_value="${ttl_value#0}" - done - [ -z "$ttl_value" ] && ttl_value=0 - - case "$ttl_value" in - ??????*) - return 1 - ;; - esac - - if [ "$ttl_value" -lt 60 ] || [ "$ttl_value" -gt 86400 ]; then - return 1 - fi - - printf "%s" "$ttl_value" -} - -_hostup_domain_in_zone() { - host="$(printf "%s" "${1%.}" | _lower_case)" - zone="$(printf "%s" "${2%.}" | _lower_case)" - - if [ -z "$host" ] || [ -z "$zone" ]; then - return 1 - fi - - if [ "$host" = "$zone" ]; then - return 0 - fi - - case "$host" in - *."$zone") - return 0 - ;; - esac - - return 1 -} - -_hostup_detect_zone() { - fulldomain="$1" - - if [ -n "$HOSTUP_ZONE_ID" ] && [ -n "$HOSTUP_ZONE_DOMAIN" ]; then - if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then - return 0 - fi - _debug "hostup_cached_zone_mismatch" "$HOSTUP_ZONE_DOMAIN" - HOSTUP_ZONE_ID="" - HOSTUP_ZONE_DOMAIN="" - fi - - HOSTUP_ZONE_DOMAIN="" - _debug "hostup_full_domain" "$fulldomain" - - if [ -n "$HOSTUP_ZONE_ID" ] && [ -z "$HOSTUP_ZONE_DOMAIN" ]; then - # Attempt to fetch domain name for provided zone ID - if _hostup_fetch_zone_details "$HOSTUP_ZONE_ID"; then - if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then - return 0 - fi - _debug "hostup_forced_zone_mismatch" "$HOSTUP_ZONE_DOMAIN" - fi - HOSTUP_ZONE_ID="" - HOSTUP_ZONE_DOMAIN="" - fi - - _domain_candidate="$(printf "%s" "${fulldomain%.}" | _lower_case)" - _debug "hostup_initial_candidate" "$_domain_candidate" - - while [ -n "$_domain_candidate" ]; do - _debug "hostup_zone_candidate" "$_domain_candidate" - if _hostup_lookup_zone "$_domain_candidate"; then - HOSTUP_ZONE_DOMAIN="$_lookup_zone_domain" - HOSTUP_ZONE_ID="$_lookup_zone_id" - return 0 - fi - - case "$_domain_candidate" in - *.*) ;; - *) break ;; - esac - - _domain_candidate="${_domain_candidate#*.}" - done - - HOSTUP_ZONE_ID="" - return 1 -} - -_hostup_record_name() { - fulldomain="$1" - zonedomain="$2" - - # Remove trailing dot, if any - fulldomain="${fulldomain%.}" - zonedomain="${zonedomain%.}" - - if [ "$fulldomain" = "$zonedomain" ]; then - printf "%s" "@" - return 0 - fi - - suffix=".$zonedomain" - case "$fulldomain" in - *"$suffix") - printf "%s" "${fulldomain%"$suffix"}" - ;; - *) - # Domain not within zone, fall back to full host - printf "%s" "$fulldomain" - ;; - esac -} - -_hostup_sanitize_name() { - name="$1" - - if [ -z "$name" ] || [ "$name" = "." ]; then - printf "%s" "@" - return 0 - fi - - # Remove any trailing dot - name="${name%.}" - printf "%s" "$name" -} - -_hostup_fqdn() { - domain="$1" - printf "%s" "${domain%.}" -} - -_hostup_fetch_zone_details() { - zone_id="$1" - - if ! _hostup_rest "GET" "/dns-zones/$zone_id/records" ""; then - return 1 - fi - - zonedomain="$(_hostup_json_extract "name" "$_hostup_response")" - if [ -n "$zonedomain" ]; then - HOSTUP_ZONE_DOMAIN="$zonedomain" - return 0 - fi - - return 1 -} - -_hostup_load_zones() { - if ! _hostup_rest "GET" "/dns-zones?limit=1000" ""; then - return 1 - fi - - HOSTUP_ZONES_CACHE="" - data="$(printf "%s" "$_hostup_response" | tr '{' '\n')" - - while IFS= read -r line; do - case "$line" in - *'"id"'*'"name"'*) - zone_id="$(_hostup_json_extract "id" "$line")" - zone_domain="$(_hostup_json_extract "name" "$line")" - if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then - HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id} -" - _debug "hostup_zone_loaded" "$zone_domain|$zone_id" - fi - ;; - esac - done </ng/profile/user/token/list -# Then generate a token with following scopes : -# - domain:read -# - dns:read -# - dns:write +# https://manager.infomaniak.com/v3//api/dashboard +# Then generate a token with the scope Domain # this is given as an environment variable INFOMANIAK_API_TOKEN # base variables @@ -67,30 +65,33 @@ dns_infomaniak_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" + fqdn=${fulldomain#_acme-challenge.} + # guess which base domain to add record to - zone=$(_get_zone "$fulldomain") - if [ -z "$zone" ]; then - _err "cannot find zone:<${zone}> to modify" + zone_and_id=$(_find_zone "$fqdn") + if [ -z "$zone_and_id" ]; then + _err "cannot find zone to modify" return 1 fi + zone=${zone_and_id% *} + domain_id=${zone_and_id#* } # extract first part of domain key=${fulldomain%."$zone"} - _debug "key:$key" - _debug "txtvalue: $txtvalue" + _debug "zone:$zone id:$domain_id key:$key" # payload data="{\"type\": \"TXT\", \"source\": \"$key\", \"target\": \"$txtvalue\", \"ttl\": $INFOMANIAK_TTL}" # API call - response=$(_post "$data" "${INFOMANIAK_API_URL}/2/zones/${zone}/records") - if _contains "$response" '"result":"success"'; then + response=$(_post "$data" "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record") + if [ -n "$response" ] && echo "$response" | _contains '"result":"success"'; then _info "Record added" - _debug "response: $response" + _debug "Response: $response" return 0 fi - _err "Could not create record." + _err "could not create record" _debug "Response: $response" return 1 } @@ -105,7 +106,7 @@ dns_infomaniak_rm() { if [ -z "$INFOMANIAK_API_TOKEN" ]; then INFOMANIAK_API_TOKEN="" - _err "Please provide a valid Infomaniak API token in variable INFOMANIAK_API_TOKEN." + _err "Please provide a valid Infomaniak API token in variable INFOMANIAK_API_TOKEN" return 1 fi @@ -129,7 +130,7 @@ dns_infomaniak_rm() { fi export _H1="Authorization: Bearer $INFOMANIAK_API_TOKEN" - export _H2="Content-Type: application/json" + export _H2="ContentType: application/json" fulldomain=$1 txtvalue=$2 @@ -137,56 +138,63 @@ dns_infomaniak_rm() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" + fqdn=${fulldomain#_acme-challenge.} + # guess which base domain to add record to - zone=$(_get_zone "$fulldomain") - if [ -z "$zone" ]; then - _err "cannot find zone:<$zone> to modify" + zone_and_id=$(_find_zone "$fqdn") + if [ -z "$zone_and_id" ]; then + _err "cannot find zone to modify" return 1 fi + zone=${zone_and_id% *} + domain_id=${zone_and_id#* } # extract first part of domain key=${fulldomain%."$zone"} - key=$(echo "$key" | _lower_case) - _debug "zone:$zone" - _debug "key:$key" + _debug "zone:$zone id:$domain_id key:$key" # find previous record - # shellcheck disable=SC2086 - response=$(_get "${INFOMANIAK_API_URL}/2/zones/${zone}/records" | sed 's/.*"data":\[\(.*\)\]}/\1/; s/},{/}{/g') - record_id=$(echo "$response" | sed -n 's/.*"id":"*\([0-9]*\)"*.*"source":"'"$key"'".*"target":"\\"'"$txtvalue"'\\"".*/\1/p') - _debug "key: $key" - _debug "txtvalue: $txtvalue" - _debug "record_id: $record_id" - + # shellcheck disable=SC1004 + record_id=$(_get "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record" | sed 's/.*"data":\[\(.*\)\]}/\1/; s/},{/}\ +{/g' | sed -n 's/.*"id":"*\([0-9]*\)"*.*"source_idn":"'"$fulldomain"'".*"target_idn":"'"$txtvalue"'".*/\1/p') if [ -z "$record_id" ]; then _err "could not find record to delete" - _debug "response: $response" return 1 fi + _debug "record_id: $record_id" # API call - response=$(_post "" "${INFOMANIAK_API_URL}/2/zones/${zone}/records/${record_id}" "" DELETE) - if _contains "$response" '"result":"success"'; then + response=$(_post "" "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record/$record_id" "" DELETE) + if [ -n "$response" ] && echo "$response" | _contains '"result":"success"'; then _info "Record deleted" - _debug "response: $response" return 0 fi - _err "Could not delete record." - _debug "Response: $response" + _err "could not delete record" return 1 } #################### Private functions below ################################## -_get_zone() { +_get_domain_id() { domain="$1" - # Whatever the domain is, you can get the fqdn with the following. - response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones") - _debug2 "_get_zone response" "$response" - if ! _contains "$response" '"result":"success"'; then - _err "cannot get zones for ${domain}, response: ${response}" - return 1 - fi - echo "$response" | _egrep_o '"fqdn" *: *"[^"]*"' | _head_n 1 | cut -d '"' -f 4 + + # shellcheck disable=SC1004 + _get "${INFOMANIAK_API_URL}/1/product?service_name=domain&customer_name=$domain" | sed 's/.*"data":\[{\(.*\)}\]}/\1/; s/,/\ +/g' | sed -n 's/^"id":\(.*\)/\1/p' +} + +_find_zone() { + zone="$1" + + # find domain in list, removing . parts sequentialy + while _contains "$zone" '\.'; do + _debug "testing $zone" + id=$(_get_domain_id "$zone") + if [ -n "$id" ]; then + echo "$zone $id" + return + fi + zone=${zone#*.} + done } diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index 460d4d28..808fc3a9 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -6,7 +6,6 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_inwx Options: INWX_User Username INWX_Password Password - INWX_Shared_Secret 2 Factor Authentication Shared Secret (optional requires oathtool) ' # Dependencies: @@ -111,17 +110,11 @@ dns_inwx_rm() { %s - - content - - %s - - - ' "$_domain" "$_sub_domain" "$txtvalue") + ' "$_domain" "$_sub_domain") response="$(_post "$xml_content" "$INWX_Api" "" "POST")" if ! _contains "$response" "Command completed successfully"; then @@ -132,7 +125,7 @@ dns_inwx_rm() { if ! printf "%s" "$response" | grep "count" >/dev/null; then _info "Do not need to delete record" else - _record_id=$(printf '%s' "$response" | _egrep_o '.*(record){1}(.*)([0-9]+){1}' | _egrep_o 'id<\/name>[0-9]+' | _egrep_o '[0-9]+') + _record_id=$(printf '%s' "$response" | _egrep_o '.*(record){1}(.*)([0-9]+){1}' | _egrep_o 'id<\/name>[0-9]+' | _egrep_o '[0-9]+') _info "Deleting record" _inwx_delete_record "$_record_id" fi @@ -307,32 +300,11 @@ _get_root() { return 1 fi - # Anchor the match to the XML tag and escape dots so $h is compared - # literally: _contains uses grep, which treats "$h" as a regex, and a - # bare "g.berlight.de" would match "berlight.de" (the 'g' from - # "" plus '.' matching '>'). See issue #5129. - _hregex=$(printf "%s" "$h" | sed 's/\./\\./g') - if _contains "$response" "$_hregex"; then + if _contains "$response" "$h"; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _domain="$h" return 0 fi - # IDN fallback: INWX returns Unicode zone names; when $h is ACE/punycode, - # encode each zone name via _idn() and compare -- no python dependency. - if _contains "$h" "xn--"; then - _zone_unicode=$(printf "%s" "$response" | _egrep_o '[^<]*' | - sed 's/<[^>]*>//g' | while IFS= read -r _z; do - if [ "$(_idn "$_z")" = "$h" ]; then - printf "%s" "$_z" - break - fi - done) - if [ -n "$_zone_unicode" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain="$_zone_unicode" - return 0 - fi - fi p=$i i=$(_math "$i" + 1) done @@ -352,7 +324,7 @@ _inwx_delete_record() { id - %s + %s @@ -390,7 +362,7 @@ _inwx_update_record() { id - %s + %s diff --git a/dnsapi/dns_ionos.sh b/dnsapi/dns_ionos.sh index 00662e82..9a464253 100755 --- a/dnsapi/dns_ionos.sh +++ b/dnsapi/dns_ionos.sh @@ -16,7 +16,7 @@ IONOS_TXT_TTL=60 # minimum accepted by API IONOS_TXT_PRIO=10 dns_ionos_add() { - fulldomain="$(echo "$1" | _lower_case)" + fulldomain=$1 txtvalue=$2 if ! _ionos_init; then @@ -34,7 +34,7 @@ dns_ionos_add() { } dns_ionos_rm() { - fulldomain="$(echo "$1" | _lower_case)" + fulldomain=$1 txtvalue=$2 if ! _ionos_init; then @@ -146,7 +146,7 @@ _ionos_rest() { if [ "$method" != "GET" ]; then export _H2="Accept: application/json" - export _H3= + export _H3="Content-Type: application/json" _response="$(_post "$data" "$IONOS_API$route" "" "$method" "application/json")" else diff --git a/dnsapi/dns_ipprojects.sh b/dnsapi/dns_ipprojects.sh deleted file mode 100644 index dadd05f0..00000000 --- a/dnsapi/dns_ipprojects.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_ipprojects_info='IP-Projects DNS -Site: ip-projects.de/ -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_ipprojects -Options: - IPP_Apikey API Key -Issues: github.com/acmesh-official/acme.sh/issues/6958 -Author: Markus Ebner -' - -IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}" -IPP_API="https://api.ip-projects.de/v1/dns/acme" - -######## Public functions ######## - -dns_ipprojects_add() { - fulldomain="$1" - txtvalue="$2" - - _info "Using IP-Projects DNS API to add record" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - if ! _IPP_load_credentials; then - return 1 - fi - - _IPP_api_request "add" "$fulldomain" "$txtvalue" -} - -dns_ipprojects_rm() { - fulldomain="$1" - txtvalue="$2" - - _info "Using IP-Projects DNS API to remove record" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - if ! _IPP_load_credentials; then - return 1 - fi - - _IPP_api_request "remove" "$fulldomain" "$txtvalue" -} - -######## Private helpers ######## - -_IPP_load_credentials() { - IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}" - - if [ -z "$IPP_Apikey" ]; then - _err "You must export IPP_Apikey" - _err "e.g.: export IPP_Apikey=\"your_api_key\"" - return 1 - fi - - _saveaccountconf_mutable IPP_Apikey "$IPP_Apikey" - return 0 -} - -_IPP_api_request() { - action="$1" - domain="$2" - value="$3" - - url="$IPP_API/$action" - - data="{\"domain\":\"$domain\",\"key\":\"$domain\",\"value\":\"$value\"}" - _debug url "$url" - _debug data "$data" - export _H1="X-API-Key: $IPP_Apikey" - - response="$(_post "$data" "$url" "" "POST" "application/json")" - ret="$?" - _ipprojects_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n') - - _debug response "$response" - - if [ "$ret" != "0" ]; then - _err "HTTP request failed" - return 1 - fi - - if [ "$_ipprojects_last_http_code" != "200" ]; then - _err "API returned an error [code: ${_ipprojects_last_http_code}]" - return 1 - fi - - return 0 -} diff --git a/dnsapi/dns_ispconfig.sh b/dnsapi/dns_ispconfig.sh index bd6bfb28..edc789e1 100755 --- a/dnsapi/dns_ispconfig.sh +++ b/dnsapi/dns_ispconfig.sh @@ -136,7 +136,7 @@ _ISPC_getZoneInfo() { curResult="$(_post "${curData}" "${ISPC_Api}?client_get_id")" _debug "Calling _ISPC_ClientGetID: '${curData}' '${ISPC_Api}?client_get_id'" _debug "Result of _ISPC_ClientGetID: '$curResult'" - client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d '[' -f 1 | tr -d '{}') + client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | tr -d '{}') _debug "Client ID: '${client_id}'" case "${client_id}" in '' | *[!0-9]*) diff --git a/dnsapi/dns_joker.sh b/dnsapi/dns_joker.sh index 0ad80327..1fe33c67 100644 --- a/dnsapi/dns_joker.sh +++ b/dnsapi/dns_joker.sh @@ -7,7 +7,7 @@ Options: JOKER_USERNAME Username JOKER_PASSWORD Password Issues: github.com/acmesh-official/acme.sh/issues/2840 -Author: @aattww +Author: ' JOKER_API="https://svc.joker.com/nic/replace" @@ -35,28 +35,9 @@ dns_joker_add() { return 1 fi - # Joker's /nic/replace overwrites all TXT records at the label on every call, - # and the API is not readable, so accumulate the values locally (keyed by the - # full record name) and re-send the whole set each time. This is required so a - # wildcard cert (base + *.domain both validating under the same - # _acme-challenge label) does not overwrite its own first challenge value. - _joker_conf_key=$(printf "%s" "JOKER_TXT_${fulldomain}" | tr '.-' '_') - _joker_values=$(_readdomainconf "$_joker_conf_key") - if [ -z "$_joker_values" ]; then - _joker_values="$txtvalue" - elif ! _contains " $_joker_values " " $txtvalue "; then - _joker_values="$_joker_values $txtvalue" - fi - - _joker_value_params="" - for _joker_v in $_joker_values; do - _joker_value_params="$_joker_value_params&value=$_joker_v" - done - _info "Adding TXT record" - if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT$_joker_value_params"; then + if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value=$txtvalue"; then if _startswith "$response" "OK"; then - _savedomainconf "$_joker_conf_key" "$_joker_values" _info "Added, OK" return 0 fi @@ -78,36 +59,10 @@ dns_joker_rm() { return 1 fi - # Remove only this value from the accumulated set and replace the label with - # whatever remains (an empty value clears the label's TXT records entirely). - _joker_conf_key=$(printf "%s" "JOKER_TXT_${fulldomain}" | tr '.-' '_') - _joker_values=$(_readdomainconf "$_joker_conf_key") - _joker_remaining="" - for _joker_v in $_joker_values; do - if [ "$_joker_v" != "$txtvalue" ]; then - _joker_remaining="$_joker_remaining $_joker_v" - fi - done - _joker_remaining=$(printf "%s" "$_joker_remaining" | sed 's/^ *//') - - _joker_value_params="" - for _joker_v in $_joker_remaining; do - _joker_value_params="$_joker_value_params&value=$_joker_v" - done - if [ -z "$_joker_value_params" ]; then - _joker_value_params="&value=" - fi - _info "Removing TXT record" - # TXT record is removed by replacing the label with the remaining values - # (or an empty value, which clears all TXT records at the label). - if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT$_joker_value_params"; then + # TXT record is removed by setting its value to empty. + if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value="; then if _startswith "$response" "OK"; then - if [ -z "$_joker_remaining" ]; then - _cleardomainconf "$_joker_conf_key" - else - _savedomainconf "$_joker_conf_key" "$_joker_remaining" - fi _info "Removed, OK" return 0 fi diff --git a/dnsapi/dns_knot.sh b/dnsapi/dns_knot.sh index 2b6d8ef4..5636804a 100644 --- a/dnsapi/dns_knot.sh +++ b/dnsapi/dns_knot.sh @@ -5,8 +5,7 @@ Site: www.knot-dns.cz/docs/2.5/html/man_knsupdate.html Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_knot Options: KNOT_SERVER Server hostname. Default: "localhost". - KNOT_KEY TSIG key data, not a file path. knsupdate "key" statement format: "[alg:]name secret". E.g. "hmac-sha256:acme_key BASE64SECRET=" - KNOT_ZONE Zone name. Optional, set it when the challenge record lives in a delegated subdomain zone. Default: the parent domain of the challenge record. + KNOT_KEY File path to TSIG key ' # See also dns_nsupdate.sh @@ -22,9 +21,6 @@ dns_knot_add() { # save the dns server and key to the account.conf file. _saveaccountconf KNOT_SERVER "${KNOT_SERVER}" _saveaccountconf KNOT_KEY "${KNOT_KEY}" - if [ -n "${KNOT_ZONE}" ]; then - _saveaccountconf KNOT_ZONE "${KNOT_ZONE}" - fi if ! _get_root "$fulldomain"; then _err "Domain does not exist." @@ -88,13 +84,6 @@ EOF # _domain=domain.com _get_root() { domain=$1 - # a delegated subdomain zone cannot be derived from the record name; - # let the user name the zone explicitly (issue 2881) - if [ -n "${KNOT_ZONE}" ]; then - _domain="${KNOT_ZONE%.}" - _debug "Using KNOT_ZONE zone" "${_domain}" - return 0 - fi i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)" i=$(_math "$i" - 1) diff --git a/dnsapi/dns_la.sh b/dnsapi/dns_la.sh index 9cb6327e..f19333c4 100644 --- a/dnsapi/dns_la.sh +++ b/dnsapi/dns_la.sh @@ -1,17 +1,14 @@ #!/usr/bin/env sh - -# LA_Id="123" -# LA_Sk="456" # shellcheck disable=SC2034 dns_la_info='dns.la Site: dns.la Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_la Options: - LA_Id APIID - LA_Sk APISecret - LA_Token 用冒号连接 APIID APISecret 再base64生成 + LA_Id API ID + LA_Key API key Issues: github.com/acmesh-official/acme.sh/issues/4257 ' + LA_Api="https://api.dns.la/api" ######## Public functions ##################### @@ -22,23 +19,18 @@ dns_la_add() { txtvalue=$2 LA_Id="${LA_Id:-$(_readaccountconf_mutable LA_Id)}" - LA_Sk="${LA_Sk:-$(_readaccountconf_mutable LA_Sk)}" - _log "LA_Id=$LA_Id" - _log "LA_Sk=$LA_Sk" + LA_Key="${LA_Key:-$(_readaccountconf_mutable LA_Key)}" - if [ -z "$LA_Id" ] || [ -z "$LA_Sk" ]; then + if [ -z "$LA_Id" ] || [ -z "$LA_Key" ]; then LA_Id="" - LA_Sk="" + LA_Key="" _err "You didn't specify a dnsla api id and key yet." return 1 fi #save the api key and email to the account conf file. _saveaccountconf_mutable LA_Id "$LA_Id" - _saveaccountconf_mutable LA_Sk "$LA_Sk" - - # generate dnsla token - _la_token + _saveaccountconf_mutable LA_Key "$LA_Key" _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -50,13 +42,11 @@ dns_la_add() { _debug _domain "$_domain" _info "Adding record" - - # record type is enum in new api, 16 for TXT - if _la_post "{\"domainId\":\"$_domain_id\",\"type\":16,\"host\":\"$_sub_domain\",\"data\":\"$txtvalue\",\"ttl\":600}" "record"; then - if _contains "$response" '"id":'; then + if _la_rest "record.ashx?cmd=create&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domainid=$_domain_id&host=$_sub_domain&recordtype=TXT&recorddata=$txtvalue&recordline="; then + if _contains "$response" '"resultid":'; then _info "Added, OK" return 0 - elif _contains "$response" '"msg":"与已有记录冲突"'; then + elif _contains "$response" '"code":532'; then _info "Already exists, OK" return 0 else @@ -64,7 +54,7 @@ dns_la_add() { return 1 fi fi - _err "Add txt record failed." + _err "Add txt record error." return 1 } @@ -75,9 +65,7 @@ dns_la_rm() { txtvalue=$2 LA_Id="${LA_Id:-$(_readaccountconf_mutable LA_Id)}" - LA_Sk="${LA_Sk:-$(_readaccountconf_mutable LA_Sk)}" - - _la_token + LA_Key="${LA_Key:-$(_readaccountconf_mutable LA_Key)}" _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -89,29 +77,27 @@ dns_la_rm() { _debug _domain "$_domain" _debug "Getting txt records" - # record type is enum in new api, 16 for TXT - if ! _la_get "recordList?pageIndex=1&pageSize=10&domainId=$_domain_id&host=$_sub_domain&type=16&data=$txtvalue"; then + if ! _la_rest "record.ashx?cmd=listn&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domainid=$_domain_id&domain=$_domain&host=$_sub_domain&recordtype=TXT&recorddata=$txtvalue"; then _err "Error" return 1 fi - if ! _contains "$response" '"id":'; then + if ! _contains "$response" '"recordid":'; then _info "Don't need to remove." return 0 fi - record_id=$(printf "%s" "$response" | grep '"id":' | _head_n 1 | sed 's/.*"id": *"\([^"]*\)".*/\1/') + record_id=$(printf "%s" "$response" | grep '"recordid":' | cut -d : -f 2 | cut -d , -f 1 | tr -d '\r' | tr -d '\n') _debug "record_id" "$record_id" if [ -z "$record_id" ]; then _err "Can not get record id to remove." return 1 fi - # remove record in new api is RESTful - if ! _la_post "" "record?id=$record_id" "DELETE"; then + if ! _la_rest "record.ashx?cmd=remove&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domainid=$_domain_id&domain=$_domain&recordid=$record_id"; then _err "Delete record error." return 1 fi - _contains "$response" '"code":200' + _contains "$response" '"code":300' } @@ -133,13 +119,12 @@ _get_root() { return 1 fi - if ! _la_get "domain?domain=$h"; then + if ! _la_rest "domain.ashx?cmd=get&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domain=$h"; then return 1 fi - if _contains "$response" '"domain":'; then - _domain_id=$(echo "$response" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p') - _log "_domain_id" "$_domain_id" + if _contains "$response" '"domainid":'; then + _domain_id=$(printf "%s" "$response" | grep '"domainid":' | cut -d : -f 2 | cut -d , -f 1 | tr -d '\r' | tr -d '\n') if [ "$_domain_id" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _domain="$h" @@ -158,21 +143,6 @@ _la_rest() { url="$LA_Api/$1" _debug "$url" - if ! response="$(_get "$url" "Authorization: Basic $LA_Token" | tr -d ' ' | tr "}" ",")"; then - _err "Error: $url" - return 1 - fi - - _debug2 response "$response" - return 0 -} - -_la_get() { - url="$LA_Api/$1" - _debug "$url" - - export _H1="Authorization: Basic $LA_Token" - if ! response="$(_get "$url" | tr -d ' ' | tr "}" ",")"; then _err "Error: $url" return 1 @@ -181,29 +151,3 @@ _la_get() { _debug2 response "$response" return 0 } - -# Usage: _la_post body url [POST|PUT|DELETE] -_la_post() { - body=$1 - url="$LA_Api/$2" - http_method=$3 - _debug "$body" - _debug "$url" - - export _H1="Authorization: Basic $LA_Token" - - if ! response="$(_post "$body" "$url" "" "$http_method")"; then - _err "Error: $url" - return 1 - fi - - _debug2 response "$response" - return 0 -} - -_la_token() { - LA_Token=$(printf "%s:%s" "$LA_Id" "$LA_Sk" | _base64) - _debug "$LA_Token" - - return 0 -} diff --git a/dnsapi/dns_laodc.sh b/dnsapi/dns_laodc.sh deleted file mode 100644 index 9f2103b3..00000000 --- a/dnsapi/dns_laodc.sh +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_laodc_info='LaoDC DNS API Server -Site: laodc.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_laodc -Options: - LaoDC_Key API Key -Issues: github.com/acmesh-official/acme.sh/issues/6973 -Author: @laodc -' - -# Usage: -# export LaoDC_Key="your-api-key" -# acme.sh --issue --dns dns_laodc -d example.la -d *.example.la --dnssleep 120 -# -# The credentials will be saved in ~/.acme.sh/account.conf - -LAODC_VER="0.1.2" -LAODC_API_ENDPOINT="https://dns.laodc.com/v1" - -######## Public functions ##################### - -# Usage: dns_laodc_add _acme-challenge.example.la ZPXvna6tBhq7XQMH7_t2WC2sg0F-BdmtmmpUJiK6Ho -dns_laodc_add() { - fulldomain=$1 - txtvalue=$2 - - _info "Using LaoDC DNS API" - - _laodc_validate_key || return 1 - - _debug "Checking root zone exists for [$fulldomain]" - if ! _get_root "$fulldomain"; then - _err "Invalid domain" - return 1 - fi - - domain_hash=$(echo "$response" | _egrep_o "\"hash\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") - _debug _domain "$_domain" - _debug _sub_domain "$_sub_domain" - _debug _domain_hash "$domain_hash" - - _info "Adding acme record" - if _laodc_api "POST" "$domain_hash" "$_sub_domain" "$txtvalue"; then - if [ "$_code" = "201" ]; then - _info "Added, OK" - return 0 - else - _err "Add TXT record error, invalid code. Code: $_code" - return 1 - fi - fi - - _err "Add TXT record error." - return 1 -} - -dns_laodc_rm() { - fulldomain=$1 - txtvalue=$2 - - _laodc_validate_key || return 1 - - _debug "Checking root zone exists for [$fulldomain]" - if ! _get_root "$fulldomain"; then - _err "Invalid domain" - return 1 - fi - - domain_hash=$(echo "$response" | _egrep_o "\"hash\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") - _debug _root_domain "$_domain" - _debug _sub_domain "$_sub_domain" - _debug _domain_hash "$domain_hash" - - _info "Deleting acme record" - if _laodc_api "DELETE" "$domain_hash" "$_sub_domain" "$txtvalue"; then - if [ "$_code" = "204" ]; then - _info "Deleted, OK" - return 0 - else - _err "Delete TXT record error, invalid code. Code: $_code" - return 1 - fi - fi - - _err "Delete TXT record error." - return 1 -} - -#################### Private functions below ################################## -# _acme-challenge.www.domain.com -# returns -# _domain=domain.com -# _sub_domain=www -_get_root() { - fqdn=$1 - p=1 - i=1 - - while true; do - h=$(printf "%s" "$fqdn" | cut -d . -f "$i"-100) - if [ -z "$h" ]; then - return 1 # not valid domain - fi - - # Check API if domain exists - if _laodc_api "GET" "$h"; then - if [ "$_code" = "200" ]; then - _domain="$h" - - # DNS alias mode - @ is alias for fqdn - _sub_domain=$(printf "%s" "$fqdn" | cut -d . -f 1-"$p") - if [ "$i" = "1" ]; then - _sub_domain="@" - fi - - return 0 - fi - fi - - p="$i" - i=$(_math "$i" + 1) - done - - return 1 -} - -_laodc_validate_key() { - LaoDC_Key="${LaoDC_Key:-$(_readaccountconf_mutable LaoDC_Key)}" - - if [ -z "$LaoDC_Key" ]; then - LaoDC_Key="" - _err "You didn't specify a LaoDC API Key yet." - _err "Please export LaoDC_Key and try again." - return 1 - fi - - # Save the api key to the account conf file. - _saveaccountconf_mutable LaoDC_Key "$LaoDC_Key" -} - -_laodc_api() { - method=$1 - domain=$2 - subdomain=$3 - value=$4 - - export _H1="Content-Type: application/json" - export _H2="User-Agent: acme.sh/$VER laodc-dns-acme-sh/$LAODC_VER" - export _H3="Authorization: Bearer $LaoDC_Key" - - case $method in - GET) - if [ -n "$subdomain" ]; then - response="$(_get "$LAODC_API_ENDPOINT/$domain/$subdomain?type=TXT")" - else - response="$(_get "$LAODC_API_ENDPOINT/$domain")" - fi - ;; - POST) - # Sanitize value input - value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g') - data="{ \"type\": \"TXT\", \"value\": \"$value\", \"ttl\": \"60\" }" - response="$(_post "$data" "$LAODC_API_ENDPOINT/$domain/$subdomain" "" "POST" "application/json")" - ;; - DELETE) - # Sanitize value input - value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g') - data="{ \"type\": \"TXT\", \"value\": \"$value\" }" - response="$(_post "$data" "$LAODC_API_ENDPOINT/$domain/$subdomain" "" "DELETE" "application/json")" - ;; - esac - - _ret=$? - - # Unset immediately after request to prevent leaks - export _H1= - export _H2= - export _H3= - - if [ "$_ret" != "0" ]; then - _err "Error $domain" - return 1 - fi - - responseHeaders="$(cat "$HTTP_HEADER")" - - if echo "$responseHeaders" | grep -i "Content-Type: *application/json" >/dev/null 2>&1; then - response="$(echo "$response" | _json_decode | _normalizeJson)" - fi - - _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" - - _debug "http response code $_code" - _debug response "$response" - return 0 -} diff --git a/dnsapi/dns_level27.sh b/dnsapi/dns_level27.sh deleted file mode 100644 index 3fbaf810..00000000 --- a/dnsapi/dns_level27.sh +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_level27_info='Level27 -Site: Level27.be -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_level27 -Options: - LEVEL27_API_KEY API key. Get one from the Level27 control panel (https://app.level27.eu/account/profile/security). -OptionsAlt: - LEVEL27_API API base URL. Optional. Default "https://api.level27.eu/v1". -Issues: github.com/acmesh-official/acme.sh/issues -Author: Jeroen Moors -' - -LEVEL27_API_DEFAULT="https://api.level27.eu/v1" - -######## Public functions ##################### - -# Usage: dns_level27_add _acme-challenge.www.example.com "TXT-value" -dns_level27_add() { - fulldomain="$(_idn "$1")" - txtvalue="$2" - - _info "Using Level27 to add a TXT record for $fulldomain" - - if ! _level27_init; then - return 1 - fi - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "Could not determine the root zone for $fulldomain at Level27." - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _level27_data="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\"}" - if ! _level27_rest POST "domains/$_domain_id/records" "$_level27_data"; then - _err "Could not add the TXT record." - return 1 - fi - - if _contains "$response" "\"id\":"; then - _info "TXT record added." - return 0 - fi - - _err "Unexpected response while adding the TXT record." - return 1 -} - -# Usage: dns_level27_rm _acme-challenge.www.example.com "TXT-value" -dns_level27_rm() { - fulldomain="$(_idn "$1")" - txtvalue="$2" - - _info "Using Level27 to remove the TXT record for $fulldomain" - - if ! _level27_init; then - return 1 - fi - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "Could not determine the root zone for $fulldomain at Level27." - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - if ! _level27_rest GET "domains/$_domain_id/records?type=TXT"; then - _err "Could not list the existing TXT records." - return 1 - fi - - _record_id="$(_level27_find_record_id "$response" "$txtvalue")" - if [ -z "$_record_id" ]; then - _info "No matching TXT record found; nothing to remove." - return 0 - fi - _debug _record_id "$_record_id" - - if ! _level27_rest DELETE "domains/$_domain_id/records/$_record_id"; then - _err "Could not remove the TXT record." - return 1 - fi - - _info "TXT record removed." - return 0 -} - -#################### Private functions below ################################## - -# Reads and validates the API credentials and endpoint, and stores them for renewals. -_level27_init() { - LEVEL27_API_KEY="${LEVEL27_API_KEY:-$(_readaccountconf_mutable LEVEL27_API_KEY)}" - if [ -z "$LEVEL27_API_KEY" ]; then - LEVEL27_API_KEY="" - _err "You must export the variable LEVEL27_API_KEY before using the Level27 DNS API." - _err "Get an API key from the Level27 control panel (https://app.level27.eu/account/profile/security)." - return 1 - fi - LEVEL27_API_KEY="$(echo "$LEVEL27_API_KEY" | tr -d '"')" - _saveaccountconf_mutable LEVEL27_API_KEY "$LEVEL27_API_KEY" - - LEVEL27_API="${LEVEL27_API:-$(_readaccountconf_mutable LEVEL27_API)}" - if [ -z "$LEVEL27_API" ]; then - LEVEL27_API="$LEVEL27_API_DEFAULT" - fi - _saveaccountconf_mutable LEVEL27_API "$LEVEL27_API" - - # Remove a trailing slash so endpoints can be appended consistently. - LEVEL27_API="$(echo "$LEVEL27_API" | sed 's#/$##')" - return 0 -} - -# Usage: _get_root _acme-challenge.www.example.com -# Splits the full domain into the registered zone and the subdomain part. -# Sets: _domain, _domain_id, _sub_domain -_get_root() { - domain=$1 - i=1 - p=1 - - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug h "$h" - if [ -z "$h" ]; then - # not valid - return 1 - fi - - if ! _level27_rest GET "domains?filter=$h"; then - return 1 - fi - - _level27_zones="$(echo "$response" | _normalizeJson)" - if _contains "$_level27_zones" "\"fullname\":\"$h\""; then - _domain_line="$(echo "$_level27_zones" | sed 's/},{/}\n{/g' | grep "\"fullname\":\"$h\"" | _head_n 1)" - _domain_id="$(echo "$_domain_line" | _egrep_o '"id":[0-9]*' | _head_n 1 | cut -d : -f 2)" - if [ "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - return 0 - fi - return 1 - fi - p=$i - i=$(_math "$i" + 1) - done - return 1 -} - -# Usage: _level27_find_record_id "" "" -# Prints the id of the TXT record whose content matches the value, or nothing. -_level27_find_record_id() { - _records="$(echo "$1" | _normalizeJson | sed 's/},{/}\n{/g')" - _wanted="$2" - _record_line="$(echo "$_records" | grep "\"content\":\"$_wanted\"" | _head_n 1)" - if [ -z "$_record_line" ]; then - # Some APIs store TXT content wrapped in quotes. - _record_line="$(echo "$_records" | grep "\"content\":\"\\\\\"$_wanted\\\\\"\"" | _head_n 1)" - fi - if [ -z "$_record_line" ]; then - return 0 - fi - echo "$_record_line" | _egrep_o '"id":[0-9]*' | _head_n 1 | cut -d : -f 2 -} - -# Usage: _level27_rest [data] -# Performs an authenticated API call and stores the body in $response. -_level27_rest() { - m="$1" - ep="$2" - data="$3" - _debug "$ep" - - export _H1="Authorization: $LEVEL27_API_KEY" - export _H2="Content-Type: application/json" - export _H3="Accept: application/json" - - if [ "$m" != "GET" ]; then - _debug2 data "$data" - response="$(_post "$data" "$LEVEL27_API/$ep" "" "$m")" - else - response="$(_get "$LEVEL27_API/$ep")" - fi - - if [ "$?" != "0" ]; then - _err "Error querying the Level27 API endpoint: $ep" - return 1 - fi - _debug2 response "$response" - return 0 -} diff --git a/dnsapi/dns_linode.sh b/dnsapi/dns_linode.sh new file mode 100755 index 00000000..d74d1fc8 --- /dev/null +++ b/dnsapi/dns_linode.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_linode_info='Linode.com (Old) + Deprecated. Use dns_linode_v4 +Site: Linode.com +Options: + LINODE_API_KEY API Key +Author: Philipp Grosswiler +' + +LINODE_API_URL="https://api.linode.com/?api_key=$LINODE_API_KEY&api_action=" + +######## Public functions ##################### + +#Usage: dns_linode_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_linode_add() { + fulldomain="${1}" + txtvalue="${2}" + + if ! _Linode_API; then + return 1 + fi + + _info "Using Linode" + _debug "Calling: dns_linode_add() '${fulldomain}' '${txtvalue}'" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Domain does not exist." + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _parameters="&DomainID=$_domain_id&Type=TXT&Name=$_sub_domain&Target=$txtvalue" + + if _rest GET "domain.resource.create" "$_parameters" && [ -n "$response" ]; then + _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1) + _debug _resource_id "$_resource_id" + + if [ -z "$_resource_id" ]; then + _err "Error adding the domain resource." + return 1 + fi + + _info "Domain resource successfully added." + return 0 + fi + + return 1 +} + +#Usage: dns_linode_rm _acme-challenge.www.domain.com +dns_linode_rm() { + fulldomain="${1}" + + if ! _Linode_API; then + return 1 + fi + + _info "Using Linode" + _debug "Calling: dns_linode_rm() '${fulldomain}'" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Domain does not exist." + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _parameters="&DomainID=$_domain_id" + + if _rest GET "domain.resource.list" "$_parameters" && [ -n "$response" ]; then + response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")" + + resource="$(echo "$response" | _egrep_o "{.*\"NAME\":\s*\"$_sub_domain\".*}")" + if [ "$resource" ]; then + _resource_id=$(printf "%s\n" "$resource" | _egrep_o "\"RESOURCEID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + if [ "$_resource_id" ]; then + _debug _resource_id "$_resource_id" + + _parameters="&DomainID=$_domain_id&ResourceID=$_resource_id" + + if _rest GET "domain.resource.delete" "$_parameters" && [ -n "$response" ]; then + _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1) + _debug _resource_id "$_resource_id" + + if [ -z "$_resource_id" ]; then + _err "Error deleting the domain resource." + return 1 + fi + + _info "Domain resource successfully deleted." + return 0 + fi + fi + + return 1 + fi + + return 0 + fi + + return 1 +} + +#################### Private functions below ################################## + +_Linode_API() { + if [ -z "$LINODE_API_KEY" ]; then + LINODE_API_KEY="" + + _err "You didn't specify the Linode API key yet." + _err "Please create your key and try again." + + return 1 + fi + + _saveaccountconf LINODE_API_KEY "$LINODE_API_KEY" +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=12345 +_get_root() { + domain=$1 + i=2 + p=1 + + if _rest GET "domain.list"; then + response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")" + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + hostedzone="$(echo "$response" | _egrep_o "{.*\"DOMAIN\":\s*\"$h\".*}")" + if [ "$hostedzone" ]; then + _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"DOMAINID\":\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") + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + fi + return 1 +} + +#method method action data +_rest() { + mtd="$1" + ep="$2" + data="$3" + + _debug mtd "$mtd" + _debug ep "$ep" + + export _H1="Accept: application/json" + export _H2="Content-Type: application/json" + + if [ "$mtd" != "GET" ]; then + # both POST and DELETE. + _debug data "$data" + response="$(_post "$data" "$LINODE_API_URL$ep" "" "$mtd")" + else + response="$(_get "$LINODE_API_URL$ep$data")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_me.sh b/dnsapi/dns_me.sh index 0966c5f1..43c903cd 100644 --- a/dnsapi/dns_me.sh +++ b/dnsapi/dns_me.sh @@ -53,8 +53,6 @@ dns_me_add() { _info "Added" #todo: check if the record takes effect return 0 - elif printf -- "%s" "$response" | grep -q "already exists"; then - _info "Record already exists, skipping." else _err "Add txt record error." return 1 @@ -140,7 +138,7 @@ _me_rest() { data="$3" _debug "$ep" - cdate=$(LC_ALL=C date -u +"%a, %d %b %Y %T %Z") + cdate=$(LANG=C date -u +"%a, %d %b %Y %T %Z") hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(printf "%s" "$ME_Secret" | _hex_dump | tr -d " ")" hex) export _H1="x-dnsme-apiKey: $ME_Key" diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh deleted file mode 100644 index 57679127..00000000 --- a/dnsapi/dns_mgwm.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_mgwm_info='mgw-media.de -Site: mgw-media.de -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mgwm -Options: - MGWM_CUSTOMER Your customer number - MGWM_API_HASH Your API Hash -Issues: github.com/acmesh-official/acme.sh/issues/6669 -' -# Base URL for the mgw-media.de API -MGWM_API_BASE="https://api.mgw-media.de/record" - -######## Public functions ##################### - -# This function is called by acme.sh to add a TXT record. -dns_mgwm_add() { - fulldomain=$1 - txtvalue=$2 - _info "Using mgw-media.de DNS API for domain $fulldomain (add record)" - _debug "fulldomain: $fulldomain" - _debug "txtvalue: $txtvalue" - - # Call the new private function to handle the API request. - # The 'add' action, fulldomain, type 'txt' and txtvalue are passed. - if _mgwm_request "add" "$fulldomain" "txt" "$txtvalue"; then - _info "TXT record for $fulldomain successfully added via mgw-media.de API." - _sleep 10 # Wait briefly for DNS propagation, a common practice in DNS-01 hooks. - return 0 - else - # Error message already logged by _mgwm_request, but a specific one here helps. - _err "mgwm_add: Failed to add TXT record for $fulldomain." - return 1 - fi -} -# This function is called by acme.sh to remove a TXT record after validation. -dns_mgwm_rm() { - fulldomain=$1 - txtvalue=$2 # This txtvalue is now used to identify the specific record to be removed. - _info "Removing TXT record for $fulldomain using mgw-media.de DNS API (remove record)" - _debug "fulldomain: $fulldomain" - _debug "txtvalue: $txtvalue" - - # Call the new private function to handle the API request. - # The 'rm' action, fulldomain, type 'txt' and txtvalue are passed. - if _mgwm_request "rm" "$fulldomain" "txt" "$txtvalue"; then - _info "TXT record for $fulldomain successfully removed via mgw-media.de API." - return 0 - else - # Error message already logged by _mgwm_request, but a specific one here helps. - _err "mgwm_rm: Failed to remove TXT record for $fulldomain." - return 1 - fi -} -#################### Private functions below ################################## - -# _mgwm_request() encapsulates the API call logic, including -# loading credentials, setting the Authorization header, and executing the request. -# Arguments: -# $1: action (e.g., "add", "rm") -# $2: fulldomain -# $3: type (e.g., "txt") -# $4: content (the txtvalue) -_mgwm_request() { - _action="$1" - _fulldomain="$2" - _type="$3" - _content="$4" - - _debug "Calling _mgwm_request for action: $_action, domain: $_fulldomain, type: $_type, content: $_content" - - # Load credentials from environment or acme.sh config - MGWM_CUSTOMER="${MGWM_CUSTOMER:-$(_readaccountconf_mutable MGWM_CUSTOMER)}" - MGWM_API_HASH="${MGWM_API_HASH:-$(_readaccountconf_mutable MGWM_API_HASH)}" - - # Check if credentials are set - if [ -z "$MGWM_CUSTOMER" ] || [ -z "$MGWM_API_HASH" ]; then - _err "You didn't specify one or more of MGWM_CUSTOMER or MGWM_API_HASH." - _err "Please check these environment variables and try again." - return 1 - fi - - # Save credentials for automatic renewal and future calls - _saveaccountconf_mutable MGWM_CUSTOMER "$MGWM_CUSTOMER" - _saveaccountconf_mutable MGWM_API_HASH "$MGWM_API_HASH" - - # Create the Basic Auth Header. acme.sh's _base64 function is used for encoding. - _credentials="$(printf "%s:%s" "$MGWM_CUSTOMER" "$MGWM_API_HASH" | _base64)" - export _H1="Authorization: Basic $_credentials" - _debug "Set Authorization Header: Basic " # Log debug message without sensitive credentials - - # Construct the API URL based on the action and provided parameters. - _request_url="${MGWM_API_BASE}/${_action}/${_fulldomain}/${_type}/${_content}" - _debug "Constructed mgw-media.de API URL for action '$_action': ${_request_url}" - - # Execute the HTTP GET request with the Authorization Header. - # The 5th parameter of _get is where acme.sh expects custom HTTP headers like Authorization. - response="$(_get "$_request_url")" - _debug "mgw-media.de API response for action '$_action': $response" - - # Check the API response for success. The API returns "OK" on success. - if [ "$response" = "OK" ]; then - _info "mgw-media.de API action '$_action' for record '$_fulldomain' successful." - return 0 - else - _err "Failed mgw-media.de API action '$_action' for record '$_fulldomain'. Unexpected API Response: '$response'" - return 1 - fi -} diff --git a/dnsapi/dns_mijnhost.sh b/dnsapi/dns_mijnhost.sh index 9f5e7710..9dafc702 100644 --- a/dnsapi/dns_mijnhost.sh +++ b/dnsapi/dns_mijnhost.sh @@ -1,15 +1,16 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 dns_mijnhost_info='mijn.host +Domains: mijn.host Site: mijn.host -Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mijnhost +Docs: https://mijn.host/api/doc/ +Issues: https://github.com/acmesh-official/acme.sh/issues/6177 +Author: peterv99 Options: MIJNHOST_API_KEY API Key -Issues: github.com/acmesh-official/acme.sh/issues/6177 -Author: @peterv99 ' -######## Public functions ###################### +######## Public functions ###################### Constants for your mijn-host API MIJNHOST_API="https://mijn.host/api/v2" # Add TXT record for domain verification diff --git a/dnsapi/dns_muumuu.sh b/dnsapi/dns_muumuu.sh deleted file mode 100755 index 8ef0b8c8..00000000 --- a/dnsapi/dns_muumuu.sh +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_muumuu_info='muumuu-domain.com -Site: muumuu-domain.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_muumuu -Options: - MUUMUU_PAT Personal Access Token (scopes: domains:read, dns:read, dns:write) -Issues: github.com/acmesh-official/acme.sh/issues/7011 -' - -MUUMUU_API="https://muumuu-domain.com/api/v2" - -######## Public functions ##################### - -dns_muumuu_add() { - fulldomain="$(echo "$1" | _lower_case)" - txtvalue="$2" - - _info "Using muumuu-domain.com DNS API" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - MUUMUU_PAT="${MUUMUU_PAT:-$(_readaccountconf_mutable MUUMUU_PAT)}" - if [ -z "$MUUMUU_PAT" ]; then - _err "MUUMUU_PAT is not set." - _err "Please create a Personal Access Token at https://muumuu-domain.com" - _err "with scopes: domains:read, dns:read, dns:write" - return 1 - fi - _saveaccountconf_mutable MUUMUU_PAT "$MUUMUU_PAT" - - if ! _muumuu_get_root "$fulldomain"; then - _err "Unable to find the root domain for $fulldomain" - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _info "Adding TXT record for ${fulldomain}" - body="{\"fqdn\":\"${fulldomain}.\",\"type\":\"TXT\",\"value\":\"${txtvalue}\",\"ttl\":3600}" - if _muumuu_rest POST "/me/domains/${_domain_id}/dns-records" "$body"; then - if [ "$_muumuu_code" = "201" ]; then - _info "TXT record added successfully" - return 0 - fi - fi - - _err "Failed to add TXT record (HTTP ${_muumuu_code})" - return 1 -} - -dns_muumuu_rm() { - fulldomain="$(echo "$1" | _lower_case)" - txtvalue="$2" - - _info "Using muumuu-domain.com DNS API" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - MUUMUU_PAT="${MUUMUU_PAT:-$(_readaccountconf_mutable MUUMUU_PAT)}" - if [ -z "$MUUMUU_PAT" ]; then - _err "MUUMUU_PAT is not set." - return 1 - fi - - if ! _muumuu_get_root "$fulldomain"; then - _err "Unable to find the root domain for $fulldomain" - return 1 - fi - _debug _domain_id "$_domain_id" - - _info "Looking up TXT record for ${fulldomain}" - if ! _muumuu_rest GET "/me/domains/${_domain_id}/dns-records?type=TXT&fqdn=${fulldomain}."; then - _err "Failed to list TXT records" - return 1 - fi - - record_id=$(echo "$response" | _egrep_o "\"id\":[0-9]+[^}]*\"value\":\"${txtvalue}\"" | _egrep_o "\"id\":[0-9]+" | _head_n 1 | cut -d: -f2) - if [ -z "$record_id" ]; then - _info "TXT record not found, nothing to remove" - return 0 - fi - _debug record_id "$record_id" - - if _muumuu_rest DELETE "/me/domains/${_domain_id}/dns-records/${record_id}"; then - if [ "$_muumuu_code" = "204" ]; then - _info "TXT record deleted successfully" - return 0 - fi - fi - - _err "Failed to delete TXT record (HTTP ${_muumuu_code})" - return 1 -} - -#################### Private functions below ################################## - -# _acme-challenge.www.example.com -# sets: -# _domain_id MU00000001 -# _sub_domain _acme-challenge.www -# _domain example.com -_muumuu_get_root() { - domain="$1" - i=1 - p=0 - h="" - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - if [ -z "$h" ]; then - return 1 - fi - if ! _muumuu_rest GET "/me/domains?fqdn=${h}&page-size=1"; then - return 1 - fi - if [ "$_muumuu_code" = "401" ] || [ "$_muumuu_code" = "403" ]; then - _err "Authentication failed (HTTP ${_muumuu_code}). Check MUUMUU_PAT." - return 1 - fi - if _contains "$response" "\"fqdn\":\"${h}\""; then - _domain_id=$(echo "$response" | _egrep_o "\"id\":\"MU[0-9]+\"" | _head_n 1 | cut -d: -f2 | tr -d '"') - _domain="$h" - if [ "$p" = "0" ]; then - _sub_domain="" - else - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - fi - return 0 - fi - p="$i" - i=$(_math "$i" + 1) - done -} - -_muumuu_rest() { - _muumuu_method="$1" - _muumuu_path="$2" - _muumuu_data="$3" - _muumuu_url="${MUUMUU_API}${_muumuu_path}" - - export _H1="Authorization: Bearer ${MUUMUU_PAT}" - export _H2="Content-Type: application/json" - export _H3="Accept: application/json" - export _H4="" - export _H5="" - - _secure_debug2 data "$_muumuu_data" - - if [ "$_muumuu_method" = "GET" ]; then - response="$(_get "$_muumuu_url")" - else - response="$(_post "$_muumuu_data" "$_muumuu_url" "" "$_muumuu_method")" - fi - _muumuu_ret="$?" - _muumuu_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" - _debug "HTTP code: ${_muumuu_code}" - _secure_debug2 response "$response" - - if [ "$_muumuu_ret" != "0" ]; then - _err "Error accessing ${_muumuu_url}" - return 1 - fi - - response="$(printf "%s" "$response" | _normalizeJson)" - return 0 -} diff --git a/dnsapi/dns_mydnsjp.sh b/dnsapi/dns_mydnsjp.sh index 4dfffaaa..336c4889 100755 --- a/dnsapi/dns_mydnsjp.sh +++ b/dnsapi/dns_mydnsjp.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_mydnsjp Options: MYDNSJP_MasterID Master ID MYDNSJP_Password Password -Author: @tkmsst +Author: epgdatacapbon ' ######## Public functions ##################### diff --git a/dnsapi/dns_mythic_beasts.sh b/dnsapi/dns_mythic_beasts.sh index a49ab8ab..1529e1e7 100755 --- a/dnsapi/dns_mythic_beasts.sh +++ b/dnsapi/dns_mythic_beasts.sh @@ -186,7 +186,7 @@ _oauth2() { _oauth2_std() { # HTTP Basic Authentication _H1="Authorization: Basic $(echo "$MB_AK:$MB_AS" | _base64)" - _H2="Accept: application/json" + _H2="Accepts: application/json" export _H1 _H2 body="grant_type=client_credentials" @@ -210,7 +210,7 @@ _oauth2_std() { } _oauth2_github() { - _H1="Accept: application/json" + _H1="Accepts: application/json" export _H1 body="{\"login\":{\"handle\":\"$MB_AK\",\"pass\":\"$MB_AS\",\"floating\":1}}" @@ -241,7 +241,7 @@ _mb_rest() { fi _H1="Authorization: Bearer $MB_TK" - _H2="Accept: application/json" + _H2="Accepts: application/json" export _H1 _H2 if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ] || [ "$m" = "DELETE" ]; then # body url [needbase64] [POST|PUT|DELETE] [ContentType] diff --git a/dnsapi/dns_namecheap.sh b/dnsapi/dns_namecheap.sh index 7035640b..5527b357 100755 --- a/dnsapi/dns_namecheap.sh +++ b/dnsapi/dns_namecheap.sh @@ -104,9 +104,6 @@ _get_root_by_getList() { return 1 fi - _namecheap_domain_list=$(echo "$response" | _egrep_o ']*') - _debug2 domain_list "$_namecheap_domain_list" - i=2 p=1 @@ -123,7 +120,7 @@ _get_root_by_getList() { return 1 fi - if ! _namecheap_is_our_dns "$h"; then + if ! _contains "$response" "$h"; then _debug "$h not found" else _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") @@ -136,29 +133,6 @@ _get_root_by_getList() { return 1 } -#Usage: _namecheap_is_our_dns -#Succeeds only when domains.getList listed exactly AND that entry is -#served by Namecheap's own DNS. A domain parked on Namecheap's webhosting DNS -#is listed with IsOurDNS="false", and every dns.getHosts/setHosts call against -#it is refused with error 2030288 "not using proper DNS servers". Accepting -#such a domain as the root zone hides a subdomain that IS delegated to -#Namecheap DNS and that the getHosts probe below would have found. -#https://github.com/acmesh-official/acme.sh/issues/7178 -_namecheap_is_our_dns() { - _namecheap_entry=$(echo "$_namecheap_domain_list" | grep -F " Name=\"$1\"" | _head_n 1) - if [ -z "$_namecheap_entry" ]; then - return 1 - fi - - _namecheap_ourdns=$(echo "$_namecheap_entry" | _egrep_o ' IsOurDNS="[^"]*' | cut -d '"' -f 2) - _debug2 "$1 IsOurDNS" "$_namecheap_ourdns" - - if [ "$_namecheap_ourdns" = "true" ]; then - return 0 - fi - return 1 -} - _get_root_by_getHosts() { i=100 p=99 @@ -290,16 +264,8 @@ _set_namecheap_TXT() { _debug hosts "$hosts" if [ -z "$hosts" ]; then - # An empty host list is only acceptable when the API positively confirms - # a successful getHosts reply: setHosts below REPLACES all records, so - # proceeding on a malformed/unparsed response would wipe the whole zone. - # https://github.com/acmesh-official/acme.sh/issues/6963 - if _contains "$response" "Status=\"OK\"" && _contains "$response" "DomainDNSGetHostsResult"; then - _debug "No existing host records, adding the TXT record as the first one" - else - _err "Hosts not found" - return 1 - fi + _err "Hosts not found" + return 1 fi _namecheap_reset_hostList diff --git a/dnsapi/dns_namecom.sh b/dnsapi/dns_namecom.sh index bd7da0c2..44549c9e 100755 --- a/dnsapi/dns_namecom.sh +++ b/dnsapi/dns_namecom.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_namecom Options: Namecom_Username Username Namecom_Token API Token -Author: @RaidenII +Author: RaidenII ' ######## Public functions ##################### @@ -15,7 +15,7 @@ Namecom_API="https://api.name.com/v4" #Usage: dns_namecom_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_namecom_add() { - fulldomain=$(_idn "$1") + fulldomain=$1 txtvalue=$2 Namecom_Username="${Namecom_Username:-$(_readaccountconf_mutable Namecom_Username)}" @@ -68,7 +68,7 @@ dns_namecom_add() { #Usage: fulldomain txtvalue #Remove the txt record after validation. dns_namecom_rm() { - fulldomain=$(_idn "$1") + fulldomain=$1 txtvalue=$2 Namecom_Username="${Namecom_Username:-$(_readaccountconf_mutable Namecom_Username)}" @@ -153,9 +153,10 @@ _namecom_get_root() { i=2 p=1 - # Probe each candidate with GetDomain (GET /v4/domains/{domainName}) instead - # of listing all domains: the list is paginated at 1000 domains per page, so - # larger accounts never found their domain on the first page. + if ! _namecom_rest GET "domains"; then + return 1 + fi + # Need to exclude the last field (tld) numfields=$(echo "$domain" | _egrep_o "\." | wc -l) while [ "$i" -le "$numfields" ]; do @@ -165,7 +166,7 @@ _namecom_get_root() { return 1 fi - if _namecom_rest GET "domains/$host" && _contains "$response" "\"domainName\":\"$host\""; then + if _contains "$response" "$host"; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _domain="$host" return 0 diff --git a/dnsapi/dns_namesilo.sh b/dnsapi/dns_namesilo.sh index df5871cf..b31e32a1 100755 --- a/dnsapi/dns_namesilo.sh +++ b/dnsapi/dns_namesilo.sh @@ -5,7 +5,7 @@ Site: NameSilo.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_namesilo Options: Namesilo_Key API Key -Author: @meowthink +Author: meowthink ' #Utilize API to finish dns-01 verifications. @@ -65,7 +65,7 @@ dns_namesilo_rm() { if _namesilo_rest GET "dnsListRecords?version=1&type=xml&key=$Namesilo_Key&domain=$_domain"; then retcode=$(printf "%s\n" "$response" | _egrep_o "300") if [ "$retcode" ]; then - _record_id=$(echo "$response" | _egrep_o "([^<]*)TXT$_sub_domain$txtvalue" | _egrep_o "([^<]*)" | sed -r "s/([^<]*)<\/record_id>/\1/" | tail -n 1) + _record_id=$(echo "$response" | _egrep_o "([^<]*)TXT$fulldomain" | _egrep_o "([^<]*)" | sed -r "s/([^<]*)<\/record_id>/\1/" | tail -n 1) _debug _record_id "$_record_id" if [ "$_record_id" ]; then _info "Successfully retrieved the record id for ACME challenge." diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh index 0c42989b..1ab47a89 100644 --- a/dnsapi/dns_nanelo.sh +++ b/dnsapi/dns_nanelo.sh @@ -27,16 +27,8 @@ dns_nanelo_add() { fi _saveaccountconf_mutable NANELO_TOKEN "$NANELO_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" - _info "Adding TXT record to ${fulldomain}" - response="$(_post "" "$NANELO_API$NANELO_TOKEN/dns/addrecord?domain=${_domain}&type=TXT&ttl=60&name=${_sub_domain}&value=${txtvalue}" "" "" "")" + response="$(_get "$NANELO_API$NANELO_TOKEN/dns/addrecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" if _contains "${response}" 'success'; then return 0 fi @@ -59,16 +51,8 @@ dns_nanelo_rm() { fi _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" - _debug "First, let's detect the root zone:" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - return 1 - fi - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - _info "Deleting resource record $fulldomain" - response="$(_post "" "$NANELO_API$NANELO_TOKEN/dns/deleterecord?domain=${_domain}&type=TXT&ttl=60&name=${_sub_domain}&value=${txtvalue}" "" "" "")" + response="$(_get "$NANELO_API$NANELO_TOKEN/dns/deleterecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" if _contains "${response}" 'success'; then return 0 fi @@ -76,45 +60,3 @@ dns_nanelo_rm() { _err "${response}" return 1 } - -#################### Private functions below ################################## -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com - -_get_root() { - fulldomain=$1 - - # Fetch all zones from Nanelo - response="$(_get "$NANELO_API$NANELO_TOKEN/dns/getzones")" || return 1 - - # Extract "zones" array into space-separated list - zones=$(echo "$response" | - tr -d ' \n' | - sed -n 's/.*"zones":\[\([^]]*\)\].*/\1/p' | - tr -d '"' | - tr , ' ') - _debug zones "$zones" - - bestzone="" - for z in $zones; do - case "$fulldomain" in - *."$z" | "$z") - if [ ${#z} -gt ${#bestzone} ]; then - bestzone=$z - fi - ;; - esac - done - - if [ -z "$bestzone" ]; then - _err "No matching zone found for $fulldomain" - return 1 - fi - - _domain="$bestzone" - _sub_domain=$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//") - - return 0 -} diff --git a/dnsapi/dns_netcup.sh b/dnsapi/dns_netcup.sh index 3b291854..8609adf6 100644 --- a/dnsapi/dns_netcup.sh +++ b/dnsapi/dns_netcup.sh @@ -33,11 +33,9 @@ dns_netcup_add() { exit=$(echo "$fulldomain" | tr -dc '.' | wc -c) exit=$(_math "$exit" + 1) i=$exit - _nc_last=$(_nc_lastlevel "$i") - _nc_found="" while - [ "$exit" -ge "$_nc_last" ] + [ "$exit" -gt 0 ] do tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit") if [ "$(_math "$i" - "$exit")" -eq 0 ]; then @@ -53,18 +51,12 @@ dns_netcup_add() { _err "$msg" return 1 else - _nc_found=1 break fi fi fi exit=$(_math "$exit" - 1) done - if [ -z "$_nc_found" ]; then - _err "$msg" - _nc_nozone "$fulldomain" - return 1 - fi logout } @@ -78,11 +70,9 @@ dns_netcup_rm() { exit=$(_math "$exit" + 1) i=$exit rec="" - _nc_last=$(_nc_lastlevel "$i") - _nc_found="" while - [ "$exit" -ge "$_nc_last" ] + [ "$exit" -gt 0 ] do tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit") if [ "$(_math "$i" - "$exit")" -eq 0 ]; then @@ -99,18 +89,12 @@ dns_netcup_rm() { _err "$msg" return 1 else - _nc_found=1 break fi fi fi exit=$(_math "$exit" - 1) done - if [ -z "$_nc_found" ]; then - _err "$msg" - _nc_nozone "$fulldomain" - return 1 - fi ida=0000 idv=0001 @@ -141,27 +125,6 @@ dns_netcup_rm() { logout } -# The zone is looked up by walking the challenge name from the right, one -# label at a time. The leftmost label is the challenge prefix, so the full -# name itself can never be a zone: asking netcup for it only returns 4013 -# "Validation Error", which would then mask the real 5028 "zone could not be -# found". Stop one label short, unless the name is too short to have a -# challenge prefix at all (manual invocation). -# levels -_nc_lastlevel() { - if [ "$1" -ge 3 ]; then - echo 2 - else - echo 1 - fi -} - -# fulldomain -_nc_nozone() { - _err "No DNS zone for $1 was found at netcup." - _err "Check that the domain belongs to the account of the configured NC_CID and that its DNS is hosted at netcup." -} - _login() { tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST") sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4) diff --git a/dnsapi/dns_nexdns.sh b/dnsapi/dns_nexdns.sh deleted file mode 100755 index e4447c0e..00000000 --- a/dnsapi/dns_nexdns.sh +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_nexdns_info='NexDNS -Site: nexdns.tech -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_nexdns -Options: - NEXDNS_Token API token. Can be created at https://nexdns.tech/settings/api-keys - NEXDNS_Api API base url. Default "https://api.nexdns.tech/v1". Optional. -Issues: github.com/acmesh-official/acme.sh/issues/7179 -Author: NexDNS -' - -NEXDNS_Api_Default="https://api.nexdns.tech/v1" - -######## Public functions ##################### - -#Usage: dns_nexdns_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_nexdns_add() { - fulldomain=$1 - txtvalue=$2 - - if ! _nexdns_init; then - return 1 - fi - - _saveaccountconf_mutable NEXDNS_Token "$NEXDNS_Token" - if [ "$NEXDNS_Api" != "$NEXDNS_Api_Default" ]; then - _saveaccountconf_mutable NEXDNS_Api "$NEXDNS_Api" - else - _clearaccountconf_mutable NEXDNS_Api - fi - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "Cannot find the zone of $fulldomain in this NexDNS account." - return 1 - fi - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - _debug _domain_id "$_domain_id" - - _info "Adding the TXT record for $fulldomain" - if ! _nexdns_rest POST "zones/$_domain_id/records" "{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}"; then - return 1 - fi - - _info "The TXT record has been added." - return 0 -} - -#Usage: dns_nexdns_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_nexdns_rm() { - fulldomain=$1 - txtvalue=$2 - - if ! _nexdns_init; then - return 1 - fi - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "Cannot find the zone of $fulldomain in this NexDNS account." - return 1 - fi - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - _debug _domain_id "$_domain_id" - - _info "Removing the TXT record for $fulldomain" - if ! _nexdns_rest GET "zones/$_domain_id/records?type=TXT&name=$_sub_domain"; then - return 1 - fi - - #All the challenge records share one name and one type, so the value is the - #only thing that tells them apart. A certificate covering example.com and - #*.example.com puts two of them at the same name at the same time. - _record_id="$(echo "$response" | tr '{' "\n" | grep -- "$txtvalue" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)" - _debug _record_id "$_record_id" - - if [ -z "$_record_id" ]; then - _info "The TXT record is already gone, nothing to remove." - return 0 - fi - - if ! _nexdns_rest DELETE "zones/$_domain_id/records/$_record_id"; then - return 1 - fi - - _info "The TXT record has been removed." - return 0 -} - -#################### Private functions below ################################## - -#Reads the token and the api url, and applies the default url. -_nexdns_init() { - NEXDNS_Token="${NEXDNS_Token:-$(_readaccountconf_mutable NEXDNS_Token)}" - NEXDNS_Api="${NEXDNS_Api:-$(_readaccountconf_mutable NEXDNS_Api)}" - - if [ -z "$NEXDNS_Token" ]; then - _err "You have not set NEXDNS_Token yet." - _err "Create one at https://nexdns.tech/settings/api-keys, on a plan that includes API access, then:" - _err "export NEXDNS_Token=\"your-api-token\"" - return 1 - fi - - if [ -z "$NEXDNS_Api" ]; then - NEXDNS_Api="$NEXDNS_Api_Default" - fi - #A trailing slash would make every request path begin with a double slash. - NEXDNS_Api="$(echo "$NEXDNS_Api" | sed 's|/*$||')" - _debug NEXDNS_Api "$NEXDNS_Api" - - return 0 -} - -#_acme-challenge.www.example.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=example.com -# _domain_id=Zm9vYmFy -_get_root() { - domain=$1 - i=1 - p=1 - - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug h "$h" - if [ -z "$h" ]; then - #not valid - return 1 - fi - - if ! _nexdns_rest GET "zones?search=$h&per_page=100"; then - return 1 - fi - - #search matches on a substring, so the page can also hold zones that merely - #contain h. Take the id of the one whose name is exactly h. - _domain_id="$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)" - if [ "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - return 0 - fi - - p=$i - i=$(_math "$i" + 1) - done -} - -#Usage: _nexdns_rest GET|POST|DELETE path [body] [attempt] -_nexdns_rest() { - m=$1 - ep=$2 - data=$3 - attempt=${4:-1} - _debug "$ep" - - export _H1="Authorization: Bearer $NEXDNS_Token" - export _H2="Content-Type: application/json" - export _H3="Accept: application/json" - - if [ "$m" = "GET" ]; then - response="$(_get "$NEXDNS_Api/$ep")" - else - _debug2 data "$data" - response="$(_post "$data" "$NEXDNS_Api/$ep" "" "$m" "application/json")" - fi - - if [ "$?" != "0" ]; then - _err "error $ep" - return 1 - fi - - #A single certificate costs a handful of requests, but a renewal sweep over - #many of them meets the account's per-minute budget, and that run is - #unattended. Retry-After is treated as a floor: an api may report the time one - #token needs at an average rate and name a second when nothing frees for a - #minute, so the wait grows on its own across attempts. - if [ "$(grep "^HTTP" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" = "429" ]; then - if [ "$attempt" -ge 4 ]; then - _err "$m $ep failed: rate limited, and the wait budget is spent" - return 1 - fi - - _retry_after="$(grep -i "^Retry-After" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d : -f 2 | tr -d " \r\n")" - _backoff="$(_math "$attempt" \* 15)" - #The header may also carry an http date. Anything but a plain count of - #seconds falls through to the backoff rather than being parsed: guessing - #wrong about a date is worse than waiting a known interval, and comparing a - #date numerically would abort the hook outright. - case "$_retry_after" in - "" | *[!0-9]*) _retry_after="$_backoff" ;; - *) - if [ "$_retry_after" -lt "$_backoff" ]; then - _retry_after="$_backoff" - fi - ;; - esac - - #A wait longer than this is a refusal rather than a schedule, and sleeping - #it out would hold the hook for the length of the window. Hand the run back - #instead, so the next cron pass picks it up. - if [ "$_retry_after" -gt 120 ]; then - _err "$m $ep failed: rate limited for ${_retry_after}s, longer than this hook will wait" - return 1 - fi - - _info "Rate limited by the NexDNS API; retrying in $_retry_after seconds." - _sleep "$_retry_after" - - _nexdns_rest "$m" "$ep" "$data" "$(_math "$attempt" + 1)" - return $? - fi - - #Whitespace between a key and its value would defeat every match made on the - #body, here and in the callers. - response="$(echo "$response" | _normalizeJson)" - _debug2 response "$response" - - #The status line decides success, not the body: a delete answers 204 with no - #body at all, and a record whose own content contains "error": would otherwise - #turn a stored value into a reported failure. The body is read only for the - #message once the status says the request was rejected. - _code="$(grep "^HTTP" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" - _debug2 _code "$_code" - case "$_code" in - "" | 2*) - return 0 - ;; - esac - - #A rejected request carries {"error":{"code":..,"message":..}}, so say what the - #api says went wrong. - _message="$(echo "$response" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)" - if [ -z "$_message" ]; then - _message="status $_code" - fi - _err "$m $ep failed: $_message" - - return 1 -} diff --git a/dnsapi/dns_njalla.sh b/dnsapi/dns_njalla.sh index 6ce51380..c410447d 100644 --- a/dnsapi/dns_njalla.sh +++ b/dnsapi/dns_njalla.sh @@ -98,7 +98,7 @@ dns_njalla_rm() { echo "$records" | while read -r record; do record_name=$(echo "$record" | _egrep_o "\"name\":\s?\"[^\"]*\"" | cut -d : -f 2 | tr -d " " | tr -d \") record_content=$(echo "$record" | _egrep_o "\"content\":\s?\"[^\"]*\"" | cut -d : -f 2 | tr -d " " | tr -d \") - record_id=$(echo "$record" | _egrep_o "\"id\":\s?\"?[^\",}]*" | cut -d : -f 2 | tr -d " " | tr -d \") + record_id=$(echo "$record" | _egrep_o "\"id\":\s?[0-9]+" | cut -d : -f 2 | tr -d " " | tr -d \") if [ "$_sub_domain" = "$record_name" ]; then if [ "$txtvalue" = "$record_content" ]; then _debug "record_id" "$record_id" diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index 8d7fe2c0..d5dbbcbc 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_nsupdate Options: NSUPDATE_SERVER Server hostname. Default: "localhost". NSUPDATE_SERVER_PORT Server port. Default: "53". - NSUPDATE_KEY File path to TSIG key. Default: "". Optional. + NSUPDATE_KEY File path to TSIG key. NSUPDATE_ZONE Domain zone to update. Optional. ' @@ -22,6 +22,8 @@ dns_nsupdate_add() { NSUPDATE_ZONE="${NSUPDATE_ZONE:-$(_readaccountconf_mutable NSUPDATE_ZONE)}" NSUPDATE_OPT="${NSUPDATE_OPT:-$(_readaccountconf_mutable NSUPDATE_OPT)}" + _checkKeyFile || return 1 + # save the dns server and key to the account conf file. _saveaccountconf_mutable NSUPDATE_SERVER "${NSUPDATE_SERVER}" _saveaccountconf_mutable NSUPDATE_SERVER_PORT "${NSUPDATE_SERVER_PORT}" @@ -31,52 +33,27 @@ dns_nsupdate_add() { [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost" [ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53 - [ -n "${NSUPDATE_KEY}" ] || NSUPDATE_KEY="" [ -n "${NSUPDATE_OPT}" ] || NSUPDATE_OPT="" - NSUPDATE_SERVER_LIST=$(printf "%s" "$NSUPDATE_SERVER" | tr ',' ' ') - _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\"" [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d" [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D" - - for NS_SERVER in $NSUPDATE_SERVER_LIST; do - _info "Updating DNS server: $NS_SERVER" - - if [ -z "${NSUPDATE_ZONE}" ]; then - #shellcheck disable=SC2086 - if [ -z "${NSUPDATE_KEY}" ]; then - nsupdate $nsdebug $NSUPDATE_OPT </dev/null; then - _debug "Record deleted" + if echo "$_return_str" | _egrep_o "\"result\":\"deleted\"" >/dev/null; then _opns_rest "POST" "/service/reconfigure" "{}" - _debug "Service reconfigured" + _debug "Record deleted" else _err "Error deleting record $_host from domain $fulldomain" return 1 fi else - _err "Error requesting deletion of record $_host from domain $fulldomain" + _err "Error deleting record $_host from domain $fulldomain" return 1 fi else @@ -151,17 +150,14 @@ _get_root() { return 1 fi _debug h "$h" - lines=$(echo "$_domain_response" | sed 's/{/\n/g') - for line in $lines; do - id=$(echo "$line" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"1\",\"type\":\"primary\",.*\"domainname\":\"${h}\"" | cut -d ':' -f 2 | cut -d '"' -f 2) - if [ -n "$id" ]; then - _debug id "$id" - _host=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain="${h}" - _domainid="${id}" - return 0 - fi - done + id=$(echo "$_domain_response" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"1\",\"type\":\"primary\",\"domainname\":\"${h}\"" | cut -d ':' -f 2 | cut -d '"' -f 2) + if [ -n "$id" ]; then + _debug id "$id" + _host=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain="${h}" + _domainid="${id}" + return 0 + fi p=$i i=$(_math "$i" + 1) done @@ -210,13 +206,13 @@ _existingchallenge() { return 1 fi _uuid="" - _uuid=$(echo "$_record_response" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"[01]\",\"domain\":\"[a-z0-9\-]*\",\"%domain\":\"$1\",\"name\":\"$2\",\"type\":\"TXT\",\"value\":\"$3\"" | cut -d ':' -f 2 | cut -d '"' -f 2) + _uuid=$(echo "$_record_response" | _egrep_o "\"uuid\":\"[^\"]*\",\"enabled\":\"[01]\",\"domain\":\"$1\",\"name\":\"$2\",\"type\":\"TXT\",\"value\":\"$3\"" | cut -d ':' -f 2 | cut -d '"' -f 2) if [ -n "$_uuid" ]; then _debug uuid "$_uuid" return 0 fi - _debug "${2}.${1} record not found" + _debug "${2}.$1{1} record not found" return 1 } diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh deleted file mode 100755 index 37177696..00000000 --- a/dnsapi/dns_opusdns.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env sh - -# shellcheck disable=SC2034 -dns_opusdns_info='OpusDNS.com -Site: OpusDNS.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_opusdns -Options: - OPUSDNS_API_Key API Key. Can be created at https://dashboard.opusdns.com/settings/api-keys - OPUSDNS_API_Endpoint API Endpoint URL. Default "https://api.opusdns.com". Optional. - OPUSDNS_TTL TTL for DNS challenge records in seconds. Default "60". Optional. -Issues: github.com/acmesh-official/acme.sh/issues/XXXX -Author: OpusDNS Team -' - -OPUSDNS_API_Endpoint_Default="https://api.opusdns.com" -OPUSDNS_TTL_Default=60 - -######## Public functions ########### - -# Add DNS TXT record -dns_opusdns_add() { - fulldomain=$1 - txtvalue=$2 - - _info "Using OpusDNS DNS API" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - if ! _opusdns_init; then - return 1 - fi - - if ! _get_zone "$fulldomain"; then - return 1 - fi - - _info "Zone: $_zone, Record: $_record_name" - - if ! _opusdns_api PATCH "/v1/dns/$_zone/records" "{\"ops\":[{\"op\":\"upsert\",\"record\":{\"name\":\"$_record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$txtvalue\\\"\"}}]}"; then - _err "Failed to add TXT record" - return 1 - fi - - _info "TXT record added successfully" - return 0 -} - -# Remove DNS TXT record -dns_opusdns_rm() { - fulldomain=$1 - txtvalue=$2 - - _info "Removing OpusDNS DNS record" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - if ! _opusdns_init; then - return 1 - fi - - if ! _get_zone "$fulldomain"; then - _err "Zone not found, cleanup skipped" - return 0 - fi - - _info "Zone: $_zone, Record: $_record_name" - - if ! _opusdns_api PATCH "/v1/dns/$_zone/records" "{\"ops\":[{\"op\":\"remove\",\"record\":{\"name\":\"$_record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$txtvalue\\\"\"}}]}"; then - _err "Warning: Failed to remove TXT record" - return 0 - fi - - _info "TXT record removed successfully" - return 0 -} - -######## Private functions ########### - -# Initialize and validate configuration -_opusdns_init() { - OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" - OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" - OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" - - if [ -z "$OPUSDNS_API_Key" ]; then - _err "OPUSDNS_API_Key not set" - return 1 - fi - - [ -z "$OPUSDNS_API_Endpoint" ] && OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" - [ -z "$OPUSDNS_TTL" ] && OPUSDNS_TTL="$OPUSDNS_TTL_Default" - - _saveaccountconf_mutable OPUSDNS_API_Key "$OPUSDNS_API_Key" - _saveaccountconf_mutable OPUSDNS_API_Endpoint "$OPUSDNS_API_Endpoint" - _saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL" - - _debug "Endpoint: $OPUSDNS_API_Endpoint" - return 0 -} - -# Make API request -# Usage: _opusdns_api METHOD PATH [DATA] -_opusdns_api() { - method=$1 - path=$2 - data=$3 - - export _H1="X-Api-Key: $OPUSDNS_API_Key" - export _H2="Content-Type: application/json" - - url="$OPUSDNS_API_Endpoint$path" - _debug2 "API: $method $url" - [ -n "$data" ] && _debug2 "Data: $data" - - if [ -n "$data" ]; then - response=$(_post "$data" "$url" "" "$method") - else - response=$(_get "$url") - fi - - if [ $? -ne 0 ]; then - _err "API request failed" - _debug "Response: $response" - return 1 - fi - - _debug2 "Response: $response" - return 0 -} - -# Detect zone from FQDN -# Sets: _zone, _record_name -_get_zone() { - domain=$(echo "$1" | sed 's/\.$//') - _debug "Finding zone for: $domain" - - i=1 - p=1 - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - - if [ -z "$h" ]; then - _err "No valid zone found for: $domain" - return 1 - fi - - _debug "Trying: $h" - if _opusdns_api GET "/v1/dns/$h" && _contains "$response" '"dnssec_status"'; then - _zone="$h" - _record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - [ -z "$_record_name" ] && _record_name="@" - return 0 - fi - - p="$i" - i=$(_math "$i" + 1) - done -} diff --git a/dnsapi/dns_ovh.sh b/dnsapi/dns_ovh.sh index df2b184d..24ad0904 100755 --- a/dnsapi/dns_ovh.sh +++ b/dnsapi/dns_ovh.sh @@ -201,7 +201,7 @@ dns_ovh_rm() { if ! _ovh_rest GET "domain/zone/$_domain/record/$rid"; then return 1 fi - if _contains "$response" "$txtvalue"; then + if _contains "$response" "\"target\":\"$txtvalue\""; then _debug "Found txt id:$rid" if ! _ovh_rest DELETE "domain/zone/$_domain/record/$rid"; then return 1 @@ -224,7 +224,7 @@ _ovh_authentication() { _H3="" _H4="" - _ovhdata='{"accessRules": [{"method": "GET","path": "/auth/time"},{"method": "GET","path": "/domain"},{"method": "GET","path": "/domain/zone/*"},{"method": "GET","path": "/domain/zone/*/record"},{"method": "GET","path": "/domain/zone/*/record/*"},{"method": "POST","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/refresh"},{"method": "PUT","path": "/domain/zone/*/record/*"},{"method": "DELETE","path": "/domain/zone/*/record/*"}],"redirection":"'$ovh_success'"}' + _ovhdata='{"accessRules": [{"method": "GET","path": "/auth/time"},{"method": "GET","path": "/domain"},{"method": "GET","path": "/domain/zone/*"},{"method": "GET","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/refresh"},{"method": "PUT","path": "/domain/zone/*/record/*"},{"method": "DELETE","path": "/domain/zone/*/record/*"}],"redirection":"'$ovh_success'"}' response="$(_post "$_ovhdata" "$OVH_API/auth/credential")" _debug3 response "$response" diff --git a/dnsapi/dns_pdns.sh b/dnsapi/dns_pdns.sh index 72a58af0..ec19ad25 100755 --- a/dnsapi/dns_pdns.sh +++ b/dnsapi/dns_pdns.sh @@ -50,9 +50,6 @@ dns_pdns_add() { PDNS_Ttl="$DEFAULT_PDNS_TTL" fi - # Ensure PDNS_Url has no trailing slash ('/') - PDNS_Url="${PDNS_Url%/}" - #save the api addr and key to the account conf file. _saveaccountconf_mutable PDNS_Url "$PDNS_Url" _saveaccountconf_mutable PDNS_ServerId "$PDNS_ServerId" @@ -189,23 +186,19 @@ _get_root() { domain=$1 i=1 + if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones"; then + _zones_response=$(echo "$response" | _normalizeJson) + fi + while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - # Probe each candidate zone with the server-side name filter instead of - # listing every zone: with large installations (100k zones) the - # unfiltered list takes minutes. Servers that ignore the parameter - # return the full list, which the check below still handles. - # https://doc.powerdns.com/authoritative/http-api/zone.html - if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones?zone=$h."; then - _zones_response=$(echo "$response" | _normalizeJson) - if _contains "$_zones_response" "\"name\":\"$h.\""; then - _domain="$h." - if [ -z "$h" ]; then - _domain="=2E" - fi - return 0 + if _contains "$_zones_response" "\"name\":\"$h.\""; then + _domain="$h." + if [ -z "$h" ]; then + _domain="=2E" fi + return 0 fi if [ -z "$h" ]; then diff --git a/dnsapi/dns_pleskxml.sh b/dnsapi/dns_pleskxml.sh index 176f329d..6b38abcb 100644 --- a/dnsapi/dns_pleskxml.sh +++ b/dnsapi/dns_pleskxml.sh @@ -8,7 +8,7 @@ Options: pleskxml_user Username pleskxml_pass Password Issues: github.com/acmesh-official/acme.sh/issues/2577 -Author: @Stilez, @romanlum +Author: Stilez, ' ## Plesk XML API described at: @@ -151,8 +151,8 @@ dns_pleskxml_rm() { # Extracting the id of the TXT record for the full domain (NOT case-sensitive) and corresponding value recid="$( _value "$reclist" | - grep -Fi "${fulldomain}." | - grep -F "${txtvalue}" | + grep -i "${fulldomain}." | + grep "${txtvalue}" | sed 's/^.*\([0-9]\{1,\}\)<\/id>.*$/\1/' )" @@ -419,7 +419,7 @@ _pleskxml_get_root_domain() { _debug "Checking if '$root_domain_name' is managed by the Plesk server..." - root_domain_id="$(_value "$output" | grep -F "$root_domain_name" | _head_n 1 | sed 's/^.*\([0-9]\{1,\}\)<\/id>.*$/\1/')" + root_domain_id="$(_value "$output" | grep "$root_domain_name" | _head_n 1 | sed 's/^.*\([0-9]\{1,\}\)<\/id>.*$/\1/')" if [ -n "$root_domain_id" ]; then # Found a match diff --git a/dnsapi/dns_poweradmin.sh b/dnsapi/dns_poweradmin.sh deleted file mode 100644 index a4c81835..00000000 --- a/dnsapi/dns_poweradmin.sh +++ /dev/null @@ -1,238 +0,0 @@ -#!/usr/bin/env sh - -# shellcheck disable=SC2034 - -# Credits to the authors of dnsapi/dns_pdns.sh as this reuses much of that code. - -dns_poweradmin_info='Poweradmin API -Site: https://www.poweradmin.org/ -Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_poweradmin -Options: -POWERADMIN_URL API URL (with scheme). E.g. "https://poweradmin.example.com" or "http://192.168.0.10:8080" -POWERADMIN_API_KEY API Token "pwa_xxxx" -POWERADMIN_API_VERSION Optionally override Poweradmin API version. -Issues: https://github.com/acmesh-official/acme.sh/issues/6912 -Author: Jakob Næss -' - -######## Public functions #################### - -# Usage: dns_poweradmin_add _acme-challenge.www.domain.com "123456789ABCDEF" -# fulldomain -# txtvalue -dns_poweradmin_add() { - fulldomain=$1 - txtvalue=$2 - - POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}" - POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}" - POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}" - POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}" - - if [ -z "$POWERADMIN_URL" ]; then - POWERADMIN_URL="" - _err "You didn't specify Poweradmin URL." - _err "Please set POWERADMIN_URL and try again." - return 1 - fi - - if [ -z "$POWERADMIN_API_KEY" ]; then - POWERADMIN_API_KEY="" - _err "You didn't specify Poweradmin token." - _err "Please set POWERADMIN_API_KEY and try again." - return 1 - fi - - # Save the api addr, key, and version to the account conf file. - _saveaccountconf_mutable POWERADMIN_URL "$POWERADMIN_URL" - _saveaccountconf_mutable POWERADMIN_API_KEY "$POWERADMIN_API_KEY" - _saveaccountconf_mutable POWERADMIN_API_VERSION "$POWERADMIN_API_VERSION" - - _debug "Detect root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - return 1 - fi - - _debug _domain "$_domain" - _debug _zone_id "$_zone_id" - - if ! _set_record "$fulldomain" "$txtvalue"; then - return 1 - fi - - return 0 -} - -# Usage: dns_poweradmin_rm _acme-challenge.www.domain.com "123456789ABCDEF" -# fulldomain -# txtvalue -dns_poweradmin_rm() { - fulldomain=$1 - txtvalue=$2 - - POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}" - POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}" - POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}" - POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}" - - _debug "Detect root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - return 1 - fi - - _debug _domain "$_domain" - _debug _zone_id "$_zone_id" - - if ! _rm_record "$fulldomain" "$txtvalue"; then - return 1 - fi - - return 0 -} - -######## Private functions below ##################### - -_set_record() { - _info "Adding TXT record" - full=$1 - new_challenge=$2 - - data='{"name":"'$full'","type":"TXT","content":"'$new_challenge'","ttl":60}' - - if ! _poweradmin_rest "POST" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records" "$data" "application/json"; then - _err "Failed to add TXT record" - return 1 - fi - - return 0 -} - -_rm_record() { - _info "Remove TXT record" - full=$1 - txtvalue=$2 - - if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records"; then - _err "Failed to retrieve records" - return 1 - fi - - # The API returns: {"success":true,"data":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]} - _txt_record_obj=$( - printf '%s\n' "$response" | - sed 's/^.*"data":\[//; s/\],"message":.*$//' | - awk '{ gsub(/},{/, "}\n{"); print }' | - grep -F "\"name\":\"$full\"" | - grep -F "\"type\":\"TXT\"" | - grep -F "\"content\":\"$txtvalue\"" | - _head_n 1 - ) - - if [ -z "$_txt_record_obj" ]; then - _info "TXT record not found for $full with content $txtvalue" - return 0 - fi - - record_id=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p' | _head_n 1) - record_type=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"type":"\([^"]*\)".*/\1/p' | _head_n 1) - record_name=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"name":"\([^"]*\)".*/\1/p' | _head_n 1) - record_content=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"content":"\([^"]*\)".*/\1/p' | _head_n 1) - - _debug2 "_txt_record_obj=$_txt_record_obj" - _debug2 "record id: $record_id" - _debug2 "record type: $record_type" - _debug2 "record name: $record_name" - _debug2 "record content: $record_content" - - if [ "$record_type" != "TXT" ]; then - _err "Refusing to delete non-TXT record id=$record_id type=$record_type name=$full" - return 1 - fi - - if ! _poweradmin_rest "DELETE" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records/$record_id"; then - _err "Failed to delete TXT record" - return 1 - fi - - _info "Record deleted successfully" - return 0 -} - -# _acme-challenge.www.domain.com -# returns -# _domain=domain.com -# _zone_id=220 -_get_root() { - domain=$1 - i=1 - - if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones"; then - _err "Failed to retrieve zones" - return 1 - fi - - _zones_response="$response" - - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - - if [ -z "$h" ]; then - _debug "Root domain not found for $domain" - return 1 - fi - - zone_obj=$( - printf '%s' "$_zones_response" | - sed 's/},{/}\n{/g' | - grep -F "\"name\":\"$h\"" | - _head_n 1 - ) - - if [ -n "$zone_obj" ]; then - _zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2) - _domain="$h" - _debug "Found zone: $_domain with id: $_zone_id" - return 0 - fi - - i=$(_math "$i" + 1) - done -} - -_poweradmin_rest() { - method=$1 - ep=$2 - data=$3 - ct=$4 - - export _H1="X-API-Key: $POWERADMIN_API_KEY" - - if [ "$method" = "GET" ]; then - response="$(_get "$POWERADMIN_URL$ep")" - else - _debug "API call: $method $ep" - _debug "Content-Type: $ct" - _debug "Payload: $data" - response="$(_post "$data" "$POWERADMIN_URL$ep" "" "$method" "$ct")" - fi - - # Clear _H1 variable - unset -v _H1 - - if [ "$?" != "0" ]; then - _err "API error on $method $ep" - _debug "Response: $response" - return 1 - fi - - if printf '%s' "$response" | grep -q '"success"[ ]*:[ ]*false'; then - _err "API reported failure on $method $ep" - _debug "Response: $response" - return 1 - fi - - _debug2 "API Response: $response" - return 0 -} diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh deleted file mode 100755 index 78756a35..00000000 --- a/dnsapi/dns_qc.sh +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_qc_info='QUIC.cloud -Site: quic.cloud -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_qc -Options: - QC_API_KEY QC API Key - QC_API_EMAIL Your account email -' - -QC_Api="https://api.quic.cloud/v2" - -######## Public functions ##################### - -#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_qc_add() { - fulldomain=$1 - txtvalue=$2 - - _debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue" - QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}" - QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}" - - if [ "$QC_API_KEY" ]; then - _saveaccountconf_mutable QC_API_KEY "$QC_API_KEY" - else - _err "You didn't specify a QUIC.cloud api key as QC_API_KEY." - _err "You can get yours from here https://my.quic.cloud/up/api." - return 1 - fi - - if ! _contains "$QC_API_EMAIL" "@"; then - _err "It seems that the QC_API_EMAIL=$QC_API_EMAIL is not a valid email address." - _err "Please check and retry." - return 1 - fi - #save the api key and email to the account conf file. - _saveaccountconf_mutable QC_API_EMAIL "$QC_API_EMAIL" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain during add" - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _debug "Getting txt records" - _qc_rest GET "zones/${_domain_id}/records" - - if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then - _err "Error failed response from QC GET: $response" - return 1 - fi - - # For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so - # we can not use updating anymore. - # count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) - # _debug count "$count" - # if [ "$count" = "0" ]; then - _info "Adding txt record" - if _qc_rest POST "zones/$_domain_id/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":1800}"; then - if _contains "$response" "$txtvalue"; then - _info "Added txt record, OK" - return 0 - elif _contains "$response" "Same record already exists"; then - _info "txt record already exists, OK" - return 0 - else - _err "Add txt record error: $response" - return 1 - fi - fi - _err "Add txt record error: POST failed: $response" - return 1 - -} - -#fulldomain txtvalue -dns_qc_rm() { - fulldomain=$1 - txtvalue=$2 - - _debug "Enter dns_qc_rm fulldomain: $fulldomain, txtvalue: $txtvalue" - QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}" - QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain during rm" - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _debug "Getting txt records" - _qc_rest GET "zones/${_domain_id}/records" - - if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then - _err "Error rm GET response: $response" - return 1 - fi - - _debug "Pre-jq response:" "$response" - # Do not use jq or subsequent code - #response=$(echo "$response" | jq ".result[] | select(.id) | select(.content == \"$txtvalue\") | select(.type == \"TXT\")") - #_debug "get txt response" "$response" - #if [ "${response}" = "" ]; then - # _info "Don't need to remove txt records." - # return 0 - #fi - #record_id=$(echo "$response" | grep \"id\" | awk -F ' ' '{print $2}' | sed 's/,$//') - #_debug "txt record_id" "$record_id" - #Instead of jq - array=$(echo "$response" | grep -o '\[[^]]*\]' | sed 's/^\[\(.*\)\]$/\1/') - if [ -z "$array" ]; then - _err "Expected array in QC response: $response" - return 1 - fi - # Temporary file to hold matched content (one per line) - tmpfile=$(_mktemp) - echo "$array" | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' >"$tmpfile" - record_id="" - - while IFS= read -r obj || [ -n "$obj" ]; do - if echo "$obj" | grep -q '"TXT"' && echo "$obj" | grep -q '"id"' && echo "$obj" | grep -q "$txtvalue"; then - _debug "response includes" "$obj" - record_id=$(echo "$obj" | sed 's/^\"id\":\([0-9]\+\).*/\1/') - break - fi - done <"$tmpfile" - - rm "$tmpfile" - - if [ -z "$record_id" ]; then - _info "TXT record, or $txtvalue not found, nothing to remove" - return 0 - fi - - #End of jq replacement - if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then - _info "Delete txt record error." - return 1 - fi - - _info "TXT Record ID: $record_id successfully deleted" - return 0 - -} - -#################### Private functions below ################################## -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com -# _domain_id=sdjkglgdfewsdfg -_get_root() { - domain=$1 - i=1 - p=1 - - h=$(printf "%s" "$domain" | cut -d . -f2-) - _debug h "$h" - if [ -z "$h" ]; then - _err "$h ($domain) is an invalid domain" - return 1 - fi - - if ! _qc_rest GET "zones"; then - _err "qc_rest failed" - return 1 - fi - - if _contains "$response" "\"name\":\"$h\"" || _contains "$response" "\"name\":\"$h.\""; then - _domain_id=$h - if [ "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - return 0 - fi - _err "Empty domain_id $h" - return 1 - fi - _err "Missing domain_id $h" - return 1 -} - -_qc_rest() { - m=$1 - ep="$2" - data="$3" - _debug "$ep" - - email_trimmed=$(echo "$QC_API_EMAIL" | tr -d '"') - token_trimmed=$(echo "$QC_API_KEY" | tr -d '"') - - export _H1="Content-Type: application/json" - export _H2="X-Auth-Email: $email_trimmed" - export _H3="X-Auth-Key: $token_trimmed" - - if [ "$m" != "GET" ]; then - _debug data "$data" - response="$(_post "$data" "$QC_Api/$ep" "" "$m")" - else - response="$(_get "$QC_Api/$ep")" - fi - - if [ "$?" != "0" ]; then - _err "error $ep" - return 1 - fi - _debug2 response "$response" - return 0 -} diff --git a/dnsapi/dns_rage4.sh b/dnsapi/dns_rage4.sh index b9abff17..ad312759 100755 --- a/dnsapi/dns_rage4.sh +++ b/dnsapi/dns_rage4.sh @@ -42,14 +42,6 @@ dns_rage4_add() { _debug _domain_id "$_domain_id" _rage4_rest "createrecord/?id=$_domain_id&name=$fulldomain&content=$unquotedtxtvalue&type=TXT&active=true&ttl=1" - - # Response after adding a TXT record should be something like this: - # {"status":true,"id":28160443,"error":null} - if ! _contains "$response" '"error":null' >/dev/null; then - _err "Error while adding TXT record: '$response'" - return 1 - fi - return 0 } @@ -71,12 +63,7 @@ dns_rage4_rm() { _debug "Getting txt records" _rage4_rest "getrecords/?id=${_domain_id}" - _record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -n 's/.*"id":\([0-9][0-9]*\),.*/\1/p') - if [ -z "$_record_id" ]; then - _err "error retrieving the record_id of the new TXT record in order to delete it, got: '$_record_id'." - return 1 - fi - + _record_id=$(echo "$response" | sed -rn 's/.*"id":([[:digit:]]+)[^\}]*'"$txtvalue"'.*/\1/p') _rage4_rest "deleterecord/?id=${_record_id}" return 0 } @@ -118,7 +105,8 @@ _rage4_rest() { token_trimmed=$(echo "$RAGE4_TOKEN" | tr -d '"') auth=$(printf '%s:%s' "$username_trimmed" "$token_trimmed" | _base64) - export _H1="Authorization: Basic $auth" + export _H1="Content-Type: application/json" + export _H2="Authorization: Basic $auth" response="$(_get "$RAGE4_Api$ep")" diff --git a/dnsapi/dns_regru.sh b/dnsapi/dns_regru.sh index edf8b464..be5ae117 100644 --- a/dnsapi/dns_regru.sh +++ b/dnsapi/dns_regru.sh @@ -96,8 +96,8 @@ _get_root() { for ITEM in ${domains_list}; do IDN_ITEM=${ITEM} - case ".${domain}" in - *.${IDN_ITEM}*) + case "${domain}" in + *${IDN_ITEM}*) _domain="$(_idn "${ITEM}")" _debug _domain "${_domain}" return 0 diff --git a/dnsapi/dns_rltx.sh b/dnsapi/dns_rltx.sh deleted file mode 100644 index 065ac177..00000000 --- a/dnsapi/dns_rltx.sh +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_rltx_info='Realtox Media Cloudpanel DNS API -Site: realtoxmedia.de -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_rltx -Options: - RLTX_Key API Key - RLTX_OrganizationID Organization ID -' - -######## Public functions ##################### - -#Usage: dns_rltx_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_rltx_add() { - fulldomain=$1 - txtvalue=$2 - - _info "Using Realtox Media Cloudpanel DNS API" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - if ! _rltx_init; then - return 1 - fi - - if ! _get_root "$fulldomain"; then - _err "Could not find matching DNS zone for $fulldomain" - return 1 - fi - - _debug _domain_id "$_domain_id" - _debug _domain "$_domain" - _debug _sub_domain "$_sub_domain" - - data="{\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"ttl\":120}" - if ! _rltx_rest POST "domains/$_domain_id/dns/acme-txt" "$data"; then - _err "Add TXT record request failed" - return 1 - fi - if _contains "$response" '"status":"added"'; then - _info "Added TXT record, OK" - return 0 - fi - _err "Add TXT record failed: $response" - return 1 -} - -#Usage: fulldomain txtvalue -#Remove the txt record after validation. -dns_rltx_rm() { - fulldomain=$1 - txtvalue=$2 - - _info "Using Realtox Media Cloudpanel DNS API" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - if ! _rltx_init; then - return 1 - fi - - if ! _get_root "$fulldomain"; then - _err "Could not find matching DNS zone for $fulldomain" - return 1 - fi - - _debug _domain_id "$_domain_id" - _debug _domain "$_domain" - _debug _sub_domain "$_sub_domain" - - data="{\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"ttl\":120}" - if ! _rltx_rest DELETE "domains/$_domain_id/dns/acme-txt" "$data"; then - _err "Remove TXT record request failed" - return 1 - fi - if _contains "$response" '"status":"removed"'; then - _info "Removed TXT record, OK" - return 0 - fi - _err "Remove TXT record failed: $response" - return 1 -} - -#################### Private functions below ################################## - -_rltx_init() { - RLTX_Key="${RLTX_Key:-$(_readaccountconf_mutable RLTX_Key)}" - RLTX_OrganizationID="${RLTX_OrganizationID:-$(_readaccountconf_mutable RLTX_OrganizationID)}" - - if [ -z "$RLTX_Key" ] || [ -z "$RLTX_OrganizationID" ]; then - RLTX_Key="" - RLTX_OrganizationID="" - _err "Please specify RLTX_Key and RLTX_OrganizationID." - _err "You can export them and retry: export RLTX_Key=... RLTX_OrganizationID=..." - return 1 - fi - - _saveaccountconf_mutable RLTX_Key "$RLTX_Key" - _saveaccountconf_mutable RLTX_OrganizationID "$RLTX_OrganizationID" -} - -_get_root() { - domain=$1 - fqdn_encoded="$(printf "%s" "$domain" | _url_encode)" - if ! _rltx_rest GET "domains/dns/acme-zone?fqdn=$fqdn_encoded"; then - return 1 - fi - if ! _contains "$response" '"domain_id":"'; then - return 1 - fi - - _domain_id="$(printf "%s" "$response" | _egrep_o '"domain_id":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)" - _domain="$(printf "%s" "$response" | _egrep_o '"zone":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)" - _sub_domain="$(printf "%s" "$response" | _egrep_o '"record_name":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)" - - if [ -z "$_domain_id" ] || [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then - return 1 - fi - return 0 -} - -_rltx_rest() { - m=$1 - ep="$2" - data="$3" - _debug "$ep" - - export _H1="X-API-Key: $RLTX_Key" - export _H2="X-Organization-ID: $RLTX_OrganizationID" - export _H3="Content-Type: application/json" - - if [ "$m" = "GET" ]; then - response="$(_get "https://api.ccp.realtoxmedia.de/api/$ep")" - else - _debug2 data "$data" - response="$(_post "$data" "https://api.ccp.realtoxmedia.de/api/$ep" "" "$m")" - fi - - if [ "$?" != "0" ]; then - _err "Realtox Media Cloudpanel API request failed: $ep" - return 1 - fi - _debug2 response "$response" - return 0 -} diff --git a/dnsapi/dns_schlundtech.sh b/dnsapi/dns_schlundtech.sh index 21930110..6d2930a2 100644 --- a/dnsapi/dns_schlundtech.sh +++ b/dnsapi/dns_schlundtech.sh @@ -7,7 +7,7 @@ Options: SCHLUNDTECH_USER Username SCHLUNDTECH_PASSWORD Password Issues: github.com/acmesh-official/acme.sh/issues/2246 -Author: @mod242 +Author: ' SCHLUNDTECH_API="https://gateway.schlundtech.de" diff --git a/dnsapi/dns_selectel.sh b/dnsapi/dns_selectel.sh index 8ba9a4fb..434bc483 100644 --- a/dnsapi/dns_selectel.sh +++ b/dnsapi/dns_selectel.sh @@ -1,21 +1,27 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 -dns_selectel_info='Selectel.com -Domains: Selectel.ru -Site: Selectel.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_selectel -Options: For old API version v1 (deprecated) - SL_Ver API version. Use "v1". - SL_Key API Key -OptionsAlt: For the current API version v2 - SL_Ver API version. Use "v2". - SL_Login_ID Account ID - SL_Project_Name Project name - SL_Login_Name Service user name - SL_Pswd Service user password - SL_Expire Token lifetime. In minutes (0-1440). Default "1400" -Issues: github.com/acmesh-official/acme.sh/issues/5126 -' + +# dns_selectel_info='Selectel.com +# Domains: Selectel.ru +# Site: Selectel.com +# Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_selectel +# Options: +# Variables that must be defined before running +# SL_Ver can take one of the values 'v1' or 'v2', default is 'v1' +# SL_Ver='v1', when using version API legacy (v1) +# SL_Ver='v2', when using version API actual (v2) +# when using API version v1, i.e. SL_Ver is 'v1' or not defined: +# SL_Key - API Key, required +# when using API version v2: +# SL_Ver - required as 'v2' +# SL_Login_ID - account ID, required +# SL_Project_Name - name project, required +# SL_Login_Name - service user name, required +# SL_Pswd - service user password, required +# SL_Expire - token lifetime in minutes (0-1440), default 1400 minutes +# +# Issues: github.com/acmesh-official/acme.sh/issues/5126 +# SL_Api="https://api.selectel.ru/domains" auth_uri="https://cloud.api.selcloud.ru/identity/v3/auth/tokens" @@ -368,7 +374,7 @@ _get_auth_token() { _data_auth="{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":{\"user\":{\"name\":\"${SL_Login_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"},\"password\":\"${SL_Pswd}\"}}},\"scope\":{\"project\":{\"name\":\"${SL_Project_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"}}}}}" export _H1="Content-Type: application/json" _result=$(_post "$_data_auth" "$auth_uri") - _token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | cut -d ':' -f 2- | tr -d ' \t\r') + _token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | sed -nE "s/[[:space:]]*x-subject-token:[[:space:]]*([[:print:]]*)(\r*)/\1/p") _dt_curr=$(date +%s) SL_Token_V2="${SL_Login_Name}${_sl_sep}${_token_keystone}${_sl_sep}${SL_Login_ID}${_sl_sep}${SL_Project_Name}${_sl_sep}${_dt_curr}" _saveaccountconf_mutable SL_Token_V2 "$SL_Token_V2" diff --git a/dnsapi/dns_selfhost.sh b/dnsapi/dns_selfhost.sh index 25130146..4912dfdf 100644 --- a/dnsapi/dns_selfhost.sh +++ b/dnsapi/dns_selfhost.sh @@ -7,7 +7,6 @@ Options: SELFHOSTDNS_USERNAME Username SELFHOSTDNS_PASSWORD Password SELFHOSTDNS_MAP Subdomain name - SELFHOSTDNS_UPDATE_URL API url. Optional. Default "https://account.selfhost.de/cgi-bin/api.pl" Issues: github.com/acmesh-official/acme.sh/issues/4291 Author: Marvin Edeler ' @@ -19,11 +18,9 @@ dns_selfhost_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txt" - DEFAULT_SELFHOSTDNS_UPDATE_URL="https://account.selfhost.de/cgi-bin/api.pl" + SELFHOSTDNS_UPDATE_URL="https://selfhost.de/cgi-bin/api.pl" # Get values, but don't save until we successfully validated - SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$(_readaccountconf_mutable SELFHOSTDNS_UPDATE_URL)}" - SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$DEFAULT_SELFHOSTDNS_UPDATE_URL}" SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}" SELFHOSTDNS_PASSWORD="${SELFHOSTDNS_PASSWORD:-$(_readaccountconf_mutable SELFHOSTDNS_PASSWORD)}" # These values are domain dependent, so read them from there @@ -42,10 +39,7 @@ dns_selfhost_add() { # only match full domains (at the beginning of the string or with a leading whitespace), # e.g. don't match mytest.example.com or sub.test.example.com for test.example.com # if the domain is defined multiple times only the last occurance will be matched - # prepend a space to each line so "start of line" and "after whitespace" - # can both be matched as "after a space/tab" (portable BRE, no ERE (^|..)) - _selfhost_tab="$(printf '\t')" - mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*:\{0,1\}[0-9]*\).*/\1/p") + mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain)(:[[:digit:]]+)([:]?[[:digit:]]*)(.*)/\2\3\4/p") _debug2 mapEntry "$mapEntry" if test -z "$mapEntry"; then _err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID" @@ -57,7 +51,7 @@ dns_selfhost_add() { rid2=$(echo "$mapEntry" | cut -d: -f3) # read last used rid domain - lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*\).*/\1/p") + lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain:[[:digit:]]+)(.*)/\2/p") _debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry" lastUsedRidForDomain=$(echo "$lastUsedRidForDomainEntry" | cut -d: -f2) @@ -90,11 +84,6 @@ dns_selfhost_add() { fi fi - # Save api url if different from default - if [ "$DEFAULT_SELFHOSTDNS_UPDATE_URL" != "$SELFHOSTDNS_UPDATE_URL" ]; then - _saveaccountconf_mutable SELFHOSTDNS_UPDATE_URL "$SELFHOSTDNS_UPDATE_URL" - fi - # Now that we know the values are good, save them _saveaccountconf_mutable SELFHOSTDNS_USERNAME "$SELFHOSTDNS_USERNAME" _saveaccountconf_mutable SELFHOSTDNS_PASSWORD "$SELFHOSTDNS_PASSWORD" diff --git a/dnsapi/dns_simply.sh b/dnsapi/dns_simply.sh index 74e891ad..e0ad16e2 100644 --- a/dnsapi/dns_simply.sh +++ b/dnsapi/dns_simply.sh @@ -8,7 +8,11 @@ Options: SIMPLY_ApiKey API Key ' -SIMPLY_Api="https://api.simply.com/2" +#SIMPLY_Api="https://api.simply.com/2/" +SIMPLY_Api_Default="https://api.simply.com/2" + +#This is used for determining success of REST call +SIMPLY_SUCCESS_CODE='"status":200' ######## Public functions ##################### #Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" @@ -68,16 +72,7 @@ dns_simply_rm() { return 1 fi - case "$_simply_http_code" in - 2*) ;; - *) - _err "Failed to fetch DNS records (HTTP $_simply_http_code)" - _err "$response" - return 1 - ;; - esac - - records=$(echo "$response" | tr '{' "\n" | grep -E 'record_id|type|data|name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ') + records=$(echo "$response" | tr '{' "\n" | grep 'record_id\|type\|data\|\name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ') nr_of_deleted_records=0 _info "Fetching txt record" @@ -100,7 +95,7 @@ dns_simply_rm() { if [ "$record_id" -gt 0 ]; then - if ! _simply_delete_record "$_domain" "$record_id"; then + if ! _simply_delete_record "$_domain" "$_sub_domain" "$record_id"; then _err "Record with id $record_id could not be deleted" return 1 fi @@ -127,9 +122,14 @@ dns_simply_rm() { #################### Private functions below ################################## _simply_load_config() { + SIMPLY_Api="${SIMPLY_Api:-$(_readaccountconf_mutable SIMPLY_Api)}" SIMPLY_AccountName="${SIMPLY_AccountName:-$(_readaccountconf_mutable SIMPLY_AccountName)}" SIMPLY_ApiKey="${SIMPLY_ApiKey:-$(_readaccountconf_mutable SIMPLY_ApiKey)}" + if [ -z "$SIMPLY_Api" ]; then + SIMPLY_Api="$SIMPLY_Api_Default" + fi + if [ -z "$SIMPLY_AccountName" ] || [ -z "$SIMPLY_ApiKey" ]; then SIMPLY_AccountName="" SIMPLY_ApiKey="" @@ -144,6 +144,9 @@ _simply_load_config() { } _simply_save_config() { + if [ "$SIMPLY_Api" != "$SIMPLY_Api_Default" ]; then + _saveaccountconf_mutable SIMPLY_Api "$SIMPLY_Api" + fi _saveaccountconf_mutable SIMPLY_AccountName "$SIMPLY_AccountName" _saveaccountconf_mutable SIMPLY_ApiKey "$SIMPLY_ApiKey" } @@ -160,39 +163,26 @@ _simply_get_all_records() { _get_root() { domain=$1 - - if ! _simply_rest GET "my/products/"; then - return 1 - fi - - case "$_simply_http_code" in - 2*) ;; - *) - _err "Failed to fetch product list (HTTP $_simply_http_code)" - _err "$response" - return 1 - ;; - esac - i=2 p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) if [ -z "$h" ]; then + #not valid return 1 fi - _domain=$(printf "%s" "$response" | tr '}' '\n' | - grep -F -e "\"object\":\"$h\"" -e "\"name\":\"$h\"" -e "\"name_idn\":\"$h\"" | - sed -n 's/.*"object":"\([^"]*\)".*/\1/p' | - _head_n 1) - - if [ -n "$_domain" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - return 0 + if ! _simply_rest GET "my/products/$h/dns/"; then + return 1 fi - _debug "No Simply.com product found for $h" + if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then + _debug "$h not found" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain="$h" + return 0 + fi p="$i" i=$(_math "$i" + 1) done @@ -204,44 +194,39 @@ _simply_add_record() { sub_domain=$2 txtval=$3 - data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 120}" + data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 3600}" if ! _simply_rest POST "my/products/$domain/dns/records/" "$data"; then - _err "Adding record not successful!" + _err "Adding record not successfull!" return 1 fi - case "$_simply_http_code" in - 2*) ;; - *) - _err "Call to API not successful (HTTP $_simply_http_code), see below message for more details" + if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then + _err "Call to API not sucessfull, see below message for more details" _err "$response" return 1 - ;; - esac + fi return 0 } _simply_delete_record() { domain=$1 - record_id=$2 + sub_domain=$2 + record_id=$3 _debug record_id "Delete record with id $record_id" if ! _simply_rest DELETE "my/products/$domain/dns/records/$record_id/"; then - _err "Deleting record not successful!" + _err "Deleting record not successfull!" return 1 fi - case "$_simply_http_code" in - 2*) ;; - *) - _err "Call to API not successful (HTTP $_simply_http_code), see below message for more details" + if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then + _err "Call to API not sucessfull, see below message for more details" _err "$response" return 1 - ;; - esac + fi return 0 } @@ -263,24 +248,17 @@ _simply_rest() { export _H2="Content-Type: application/json" - : >"$HTTP_HEADER" - if [ "$m" != "GET" ]; then response="$(_post "$data" "$SIMPLY_Api/$ep" "" "$m")" else response="$(_get "$SIMPLY_Api/$ep")" fi - _ret="$?" - unset _H1 _H2 - - if [ "$_ret" != "0" ]; then + if [ "$?" != "0" ]; then _err "error $ep" return 1 fi - _simply_http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d' ' -f2 | tr -d '\r\n')" - response="$(echo "$response" | _normalizeJson)" _debug2 response "$response" diff --git a/dnsapi/dns_sitehost.sh b/dnsapi/dns_sitehost.sh deleted file mode 100755 index 94a0ee93..00000000 --- a/dnsapi/dns_sitehost.sh +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_sitehost_info='SiteHost -Site: sitehost.nz -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sitehost -Options: - SITEHOST_API_KEY API Key - SITEHOST_CLIENT_ID Client ID. The numeric client ID for your SiteHost account. -Issues: github.com/acmesh-official/acme.sh/issues/6892 -Author: Jordan Russell -' - -SITEHOST_API="https://api.sitehost.nz/1.5" - -######## Public functions ##################### - -# Usage: dns_sitehost_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_sitehost_add() { - fulldomain=$1 - txtvalue=$2 - - if ! _sitehost_load_creds; then - return 1 - fi - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - return 1 - fi - - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - # SiteHost expects the full record name as the name parameter - _info "Adding TXT record for ${fulldomain}" - if _sitehost_rest POST "dns/add_record.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)&type=TXT&name=$(printf '%s' "${fulldomain}" | _url_encode)&content=$(printf '%s' "${txtvalue}" | _url_encode)"; then - if _contains "$response" '"status":true'; then - _info "TXT record added successfully." - return 0 - fi - fi - - _err "Could not add TXT record for ${fulldomain}" - _err "$response" - return 1 -} - -# Usage: dns_sitehost_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -# Remove the txt record after validation. -dns_sitehost_rm() { - fulldomain=$1 - txtvalue=$2 - - if ! _sitehost_load_creds; then - return 1 - fi - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - return 1 - fi - - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _debug "Getting TXT records for ${_domain}" - if ! _sitehost_rest GET "dns/list_records.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)"; then - _err "Could not list DNS records" - _err "$response" - return 1 - fi - - if ! _contains "$response" '"status":true'; then - _err "Error listing DNS records" - _err "$response" - return 1 - fi - - # Extract record ID matching our fulldomain, type TXT, and txtvalue - # Response format: {"return":[{"id":"123","name":"...","type":"TXT","content":"..."},...]} - # SiteHost returns flat single-line JSON objects in the records array - # Escape regex metacharacters in values before grep matching - _fulldomain_grep="$(printf "%s" "$fulldomain" | sed 's/[][\\.^$*]/\\&/g')" - _txtvalue_grep="$(printf "%s" "$txtvalue" | sed 's/[][\\.^$*]/\\&/g')" - # Use field-specific matching to avoid false positives from substring matches - _record_id="$(echo "$response" | _egrep_o '\{[^}]*\}' | grep '"name" *: *"'"${_fulldomain_grep}"'"' | grep '"type" *: *"TXT"' | grep '"content" *: *"'"${_txtvalue_grep}"'"' | _head_n 1 | _egrep_o '"id" *: *"?[0-9]+"?' | _egrep_o '[0-9]+')" - - if [ -z "$_record_id" ]; then - _info "TXT record not found, nothing to remove." - return 0 - fi - - _debug _record_id "$_record_id" - - _info "Deleting TXT record ${_record_id} for ${fulldomain}" - if _sitehost_rest POST "dns/delete_record.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)&record_id=$(printf '%s' "${_record_id}" | _url_encode)"; then - if _contains "$response" '"status":true'; then - _info "TXT record deleted successfully." - return 0 - fi - fi - - _err "Could not delete TXT record for ${fulldomain}" - _err "$response" - return 1 -} - -#################### Private functions below ################################## - -_sitehost_load_creds() { - SITEHOST_API_KEY="${SITEHOST_API_KEY:-$(_readaccountconf_mutable SITEHOST_API_KEY)}" - SITEHOST_CLIENT_ID="${SITEHOST_CLIENT_ID:-$(_readaccountconf_mutable SITEHOST_CLIENT_ID)}" - - if [ -z "$SITEHOST_API_KEY" ] || [ -z "$SITEHOST_CLIENT_ID" ]; then - SITEHOST_API_KEY="" - SITEHOST_CLIENT_ID="" - _err "You didn't specify SITEHOST_API_KEY and/or SITEHOST_CLIENT_ID." - _err "Please export them and try again." - return 1 - fi - - _saveaccountconf_mutable SITEHOST_API_KEY "$SITEHOST_API_KEY" - _saveaccountconf_mutable SITEHOST_CLIENT_ID "$SITEHOST_CLIENT_ID" - return 0 -} - -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com -_get_root() { - domain=$1 - - _debug "Getting domain list" - - # Fetch ALL pages of domains first so we can match the most specific zone - # (a more specific zone on a later page must take precedence over a broader match) - _all_domains="" - _page=1 - - while true; do - if ! _sitehost_rest GET "dns/list_domains.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&filters%5Bpage_number%5D=${_page}"; then - _err "Could not list domains" - return 1 - fi - - if ! _contains "$response" '"status":true'; then - _err "Error listing domains" - _err "$response" - return 1 - fi - - _all_domains="${_all_domains} ${response}" - - _total_pages=$(echo "$response" | _egrep_o '"total_pages" *: *[0-9]+' | _egrep_o '[0-9]+') - if [ -z "$_total_pages" ] || [ "$_page" -ge "$_total_pages" ]; then - break - fi - - _page=$(_math "$_page" + 1) - done - - # Try each subdomain level, most specific first - _i=1 - _p=1 - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "${_i}"-100) - _debug h "$h" - if [ -z "$h" ]; then - return 1 - fi - - if echo "$_all_domains" | grep -F "\"${h}\"" >/dev/null 2>&1; then - if [ "$_i" = "1" ]; then - # DNS alias mode - fulldomain is the zone itself - _sub_domain="" - else - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"${_p}") - fi - _domain="${h}" - return 0 - fi - - _p="${_i}" - _i=$(_math "$_i" + 1) - done - - return 1 -} - -# Usage: _sitehost_rest method endpoint data -_sitehost_rest() { - m="$1" - ep="$2" - data="$3" - url="${SITEHOST_API}/${ep}" - - _debug url "$url" - - _apikey="$(printf "%s" "${SITEHOST_API_KEY}" | _url_encode)" - - if [ "$m" = "GET" ]; then - response="$(_get "${url}?apikey=${_apikey}&${data}")" - else - _debug2 data "$data" - response="$(_post "apikey=${_apikey}&${data}" "$url")" - fi - - if [ "$?" != "0" ]; then - _err "error ${ep}" - return 1 - fi - - response="$(printf '%s' "$response" | tr -d '\r')" - - _debug2 response "$response" - return 0 -} diff --git a/dnsapi/dns_sotoon.sh b/dnsapi/dns_sotoon.sh deleted file mode 100644 index b94a220f..00000000 --- a/dnsapi/dns_sotoon.sh +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_sotoon_info='Sotoon.ir -Site: Sotoon.ir -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sotoon -Options: - Sotoon_Token API Token - Sotoon_WorkspaceUUID Workspace UUID -Issues: github.com/acmesh-official/acme.sh/issues/6656 -Author: Erfan Gholizade -' - -SOTOON_API_URL="https://api.sotoon.ir/delivery/v2.1/global" - -######## Public functions ##################### - -#Adding the txt record for validation. -#Usage: dns_sotoon_add fulldomain TXT_record -#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_sotoon_add() { - fulldomain=$1 - txtvalue=$2 - _info_sotoon "Using Sotoon" - - Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}" - Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}" - - if [ -z "$Sotoon_Token" ]; then - _err_sotoon "You didn't specify \"Sotoon_Token\" token yet." - _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/tokens" - return 1 - fi - if [ -z "$Sotoon_WorkspaceUUID" ]; then - _err_sotoon "You didn't specify \"Sotoon_WorkspaceUUID\" Workspace UUID yet." - _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces" - return 1 - fi - - #save the info to the account conf file. - _saveaccountconf_mutable Sotoon_Token "$Sotoon_Token" - _saveaccountconf_mutable Sotoon_WorkspaceUUID "$Sotoon_WorkspaceUUID" - - _debug_sotoon "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err_sotoon "invalid domain" - return 1 - fi - - _info_sotoon "Adding record" - - _debug_sotoon _domain_id "$_domain_id" - _debug_sotoon _sub_domain "$_sub_domain" - _debug_sotoon _domain "$_domain" - - # First, GET the current domain zone to check for existing TXT records - # This is needed for wildcard certs which require multiple TXT values - _info_sotoon "Checking for existing TXT records" - if ! _sotoon_rest GET "$_domain_id"; then - _err_sotoon "Failed to get domain zone" - return 1 - fi - - # Check if there are existing TXT records for this subdomain - _existing_txt="" - if _contains "$response" "\"$_sub_domain\""; then - _debug_sotoon "Found existing records for $_sub_domain" - # Extract existing TXT values from the response - # The format is: "_acme-challenge":[{"TXT":"value1","type":"TXT","ttl":10},{"TXT":"value2",...}] - _existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://") - _debug_sotoon "Existing TXT records: $_existing_txt" - fi - - # Build the new record entry - _new_record="{\"TXT\":\"$txtvalue\",\"type\":\"TXT\",\"ttl\":120}" - - # If there are existing records, append to them; otherwise create new array - if [ -n "$_existing_txt" ] && [ "$_existing_txt" != "[]" ] && [ "$_existing_txt" != "null" ]; then - # Check if this exact TXT value already exists (avoid duplicates) - if _contains "$_existing_txt" "\"$txtvalue\""; then - _info_sotoon "TXT record already exists, skipping" - return 0 - fi - # Remove the closing bracket and append new record - _combined_records="$(echo "$_existing_txt" | sed 's/]$//'),$_new_record]" - _debug_sotoon "Combined records: $_combined_records" - else - # No existing records, create new array - _combined_records="[$_new_record]" - fi - - # Prepare the DNS record data in Kubernetes CRD format - _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_combined_records}}}" - - _debug_sotoon "DNS record payload: $_dns_record" - - # Use PATCH to update/add the record to the domain zone - _info_sotoon "Updating domain zone $_domain_id with TXT record" - if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then - if _contains "$response" "$txtvalue" || _contains "$response" "\"$_sub_domain\""; then - _info_sotoon "Added, OK" - return 0 - else - _debug_sotoon "Response: $response" - _err_sotoon "Add txt record error." - return 1 - fi - fi - - _err_sotoon "Add txt record error." - return 1 -} - -#Remove the txt record after validation. -#Usage: dns_sotoon_rm fulldomain TXT_record -#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_sotoon_rm() { - fulldomain=$1 - txtvalue=$2 - _info_sotoon "Using Sotoon" - _debug_sotoon fulldomain "$fulldomain" - _debug_sotoon txtvalue "$txtvalue" - - Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}" - Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}" - - _debug_sotoon "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err_sotoon "invalid domain" - return 1 - fi - _debug_sotoon _domain_id "$_domain_id" - _debug_sotoon _sub_domain "$_sub_domain" - _debug_sotoon _domain "$_domain" - - _info_sotoon "Removing TXT record" - - # First, GET the current domain zone to check for existing TXT records - if ! _sotoon_rest GET "$_domain_id"; then - _err_sotoon "Failed to get domain zone" - return 1 - fi - - # Check if there are existing TXT records for this subdomain - _existing_txt="" - if _contains "$response" "\"$_sub_domain\""; then - _debug_sotoon "Found existing records for $_sub_domain" - _existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://") - _debug_sotoon "Existing TXT records: $_existing_txt" - fi - - # If no existing records, nothing to remove - if [ -z "$_existing_txt" ] || [ "$_existing_txt" = "[]" ] || [ "$_existing_txt" = "null" ]; then - _info_sotoon "No TXT records found, nothing to remove" - return 0 - fi - - # Remove the specific TXT value from the array - # This handles the case where there are multiple TXT values (wildcard certs) - _remaining_records=$(echo "$_existing_txt" | sed "s/{\"TXT\":\"$txtvalue\"[^}]*},*//g" | sed 's/,]/]/g' | sed 's/\[,/[/g') - _debug_sotoon "Remaining records after removal: $_remaining_records" - - # If no records remain, set to null to remove the subdomain entirely - if [ "$_remaining_records" = "[]" ] || [ -z "$_remaining_records" ]; then - _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":null}}}" - else - _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_remaining_records}}}" - fi - - _debug_sotoon "Remove record payload: $_dns_record" - - # Use PATCH to remove the record from the domain zone - if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then - _info_sotoon "Record removed, OK" - return 0 - else - _debug_sotoon "Response: $response" - _err_sotoon "Error removing record" - return 1 - fi -} - -#################### Private functions below ################################## - -_get_root() { - domain=$1 - i=1 - p=1 - - _debug_sotoon "Getting root domain for: $domain" - _debug_sotoon "Sotoon WorkspaceUUID: $Sotoon_WorkspaceUUID" - - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug_sotoon "Checking domain part: $h" - - if [ -z "$h" ]; then - #not valid - _err_sotoon "Could not find valid domain" - return 1 - fi - - _debug_sotoon "Fetching domain zones from Sotoon API" - if ! _sotoon_rest GET ""; then - _err_sotoon "Failed to get domain zones from Sotoon API" - _err_sotoon "Please check your Sotoon_Token, Sotoon_WorkspaceUUID" - return 1 - fi - - _debug2_sotoon "API Response: $response" - - # Check if the response contains our domain - # Sotoon API uses Kubernetes CRD format with spec.origin for domain matching - if _contains "$response" "\"origin\":\"$h\""; then - _debug_sotoon "Found domain by origin: $h" - - # In Kubernetes CRD format, the metadata.name is the resource identifier - # The name can be either: - # 1. Same as origin - # 2. Origin with dots replaced by hyphens - # We check both patterns in the response to determine which one exists - - # Convert origin to hyphenated version for checking - _h_hyphenated=$(echo "$h" | tr '.' '-') - - # Check if the hyphenated name exists in the response - if _contains "$response" "\"name\":\"$_h_hyphenated\""; then - _domain_id="$_h_hyphenated" - _debug_sotoon "Found domain ID (hyphenated): $_domain_id" - # Check if the origin itself is used as name - elif _contains "$response" "\"name\":\"$h\""; then - _domain_id="$h" - _debug_sotoon "Found domain ID (same as origin): $_domain_id" - else - # Fallback: use the hyphenated version (more common) - _domain_id="$_h_hyphenated" - _debug_sotoon "Using hyphenated domain ID as fallback: $_domain_id" - fi - - if [ -n "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - _debug_sotoon "Domain ID (metadata.name): $_domain_id" - _debug_sotoon "Sub domain: $_sub_domain" - _debug_sotoon "Domain (origin): $_domain" - return 0 - fi - _err_sotoon "Found domain $h but could not extract domain ID" - return 1 - fi - p=$i - i=$(_math "$i" + 1) - done - return 1 -} - -_sotoon_rest() { - mtd="$1" - resource_id="$2" - data="$3" - - token_trimmed=$(echo "$Sotoon_Token" | tr -d '"') - - # Construct the API endpoint - _api_path="$SOTOON_API_URL/workspaces/$Sotoon_WorkspaceUUID/domainzones" - - if [ -n "$resource_id" ]; then - _api_path="$_api_path/$resource_id" - fi - - _debug_sotoon "API Path: $_api_path" - _debug_sotoon "Method: $mtd" - - # Set authorization header - Sotoon API uses Bearer token - export _H1="Authorization: Bearer $token_trimmed" - - if [ "$mtd" = "GET" ]; then - # GET request - _debug_sotoon "GET" "$_api_path" - response="$(_get "$_api_path")" - elif [ "$mtd" = "PATCH" ]; then - # PATCH Request - export _H2="Content-Type: application/merge-patch+json" - _debug_sotoon data "$data" - response="$(_post "$data" "$_api_path" "" "$mtd")" - else - _err_sotoon "Unknown method: $mtd" - return 1 - fi - - _debug2_sotoon response "$response" - return 0 -} - -#Wrappers for logging -_info_sotoon() { - _info "[Sotoon]" "$@" -} - -_err_sotoon() { - _err "[Sotoon]" "$@" -} - -_debug_sotoon() { - _debug "[Sotoon]" "$@" -} - -_debug2_sotoon() { - _debug2 "[Sotoon]" "$@" -} diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh deleted file mode 100644 index 8fff4037..00000000 --- a/dnsapi/dns_spaceship.sh +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_spaceship_info='Spaceship.com -Site: Spaceship.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_spaceship -Options: - SPACESHIP_API_KEY API Key - SPACESHIP_API_SECRET API Secret - SPACESHIP_ROOT_DOMAIN Root domain. Manually specify the root domain if auto-detection fails. Optional. -Issues: github.com/acmesh-official/acme.sh/issues/6304 -Author: Meow <@Meo597> -' - -# Spaceship API -# https://docs.spaceship.dev/ - -######## Public functions ##################### - -SPACESHIP_API_BASE="https://spaceship.dev/api/v1" - -# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -# Used to add txt record -dns_spaceship_add() { - fulldomain="$1" - txtvalue="$2" - - _info "Adding TXT record for $fulldomain with value $txtvalue" - - # Initialize API credentials and headers - if ! _spaceship_init; then - return 1 - fi - - # Detect root zone - if ! _get_root "$fulldomain"; then - return 1 - fi - - # Extract subdomain part relative to root domain - subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//") - if [ "$subdomain" = "$fulldomain" ]; then - _err "Failed to extract subdomain from $fulldomain relative to root domain $_domain" - return 1 - fi - _debug "Extracted subdomain: $subdomain for root domain: $_domain" - - # Escape txtvalue to prevent JSON injection (e.g., quotes in txtvalue) - escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g') - - # Prepare payload and URL for adding TXT record - # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API - payload="{\"force\": true, \"items\": [{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\", \"ttl\": 600}]}" - url="$SPACESHIP_API_BASE/dns/records/$_domain" - - # Send API request - if _spaceship_api_request "PUT" "$url" "$payload"; then - _info "Successfully added TXT record for $fulldomain" - return 0 - else - _err "Failed to add TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain." - return 1 - fi -} - -# Usage: fulldomain txtvalue -# Used to remove the txt record after validation -dns_spaceship_rm() { - fulldomain="$1" - txtvalue="$2" - - _info "Removing TXT record for $fulldomain with value $txtvalue" - - # Initialize API credentials and headers - if ! _spaceship_init; then - return 1 - fi - - # Detect root zone - if ! _get_root "$fulldomain"; then - return 1 - fi - - # Extract subdomain part relative to root domain - subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//") - if [ "$subdomain" = "$fulldomain" ]; then - _err "Failed to extract subdomain from $fulldomain relative to root domain $_domain" - return 1 - fi - _debug "Extracted subdomain: $subdomain for root domain: $_domain" - - # Escape txtvalue to prevent JSON injection - escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g') - - # Prepare payload and URL for deleting TXT record - # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API - payload="[{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}]" - url="$SPACESHIP_API_BASE/dns/records/$_domain" - - # Send API request - if _spaceship_api_request "DELETE" "$url" "$payload"; then - _info "Successfully deleted TXT record for $fulldomain" - return 0 - else - _err "Failed to delete TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain." - return 1 - fi -} - -#################### Private functions below ################################## - -_spaceship_init() { - SPACESHIP_API_KEY="${SPACESHIP_API_KEY:-$(_readaccountconf_mutable SPACESHIP_API_KEY)}" - SPACESHIP_API_SECRET="${SPACESHIP_API_SECRET:-$(_readaccountconf_mutable SPACESHIP_API_SECRET)}" - - if [ -z "$SPACESHIP_API_KEY" ] || [ -z "$SPACESHIP_API_SECRET" ]; then - _err "Spaceship API credentials are not set. Please set SPACESHIP_API_KEY and SPACESHIP_API_SECRET." - _err "Ensure \"$LE_CONFIG_HOME\" directory has restricted permissions (chmod 700 \"$LE_CONFIG_HOME\") to protect credentials." - return 1 - fi - - # Save credentials to account config for future renewals - _saveaccountconf_mutable SPACESHIP_API_KEY "$SPACESHIP_API_KEY" - _saveaccountconf_mutable SPACESHIP_API_SECRET "$SPACESHIP_API_SECRET" - - # Set common headers for API requests - export _H1="X-API-Key: $SPACESHIP_API_KEY" - export _H2="X-API-Secret: $SPACESHIP_API_SECRET" - export _H3="Content-Type: application/json" - return 0 -} - -_get_root() { - domain="$1" - - # Check manual override - SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readdomainconf SPACESHIP_ROOT_DOMAIN)}" - if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then - _domain="$SPACESHIP_ROOT_DOMAIN" - _debug "Using manually specified or saved root domain: $_domain" - _savedomainconf SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN" - return 0 - fi - - _debug "Detecting root zone for '$domain'" - - i=1 - p=1 - while true; do - _cutdomain=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - - _debug "Attempt i=$i: Checking if '$_cutdomain' is root zone (cut ret=$?)" - - if [ -z "$_cutdomain" ]; then - _debug "Cut resulted in empty string, root zone not found." - break - fi - - # Call the API to check if this _cutdomain is a manageable zone - if _spaceship_api_request "GET" "$SPACESHIP_API_BASE/dns/records/$_cutdomain?take=1&skip=0"; then - # API call succeeded (HTTP 200 OK for GET /dns/records) - _domain="$_cutdomain" - _debug "Root zone found: '$_domain'" - - # Save the detected root domain - _savedomainconf SPACESHIP_ROOT_DOMAIN "$_domain" - _info "Root domain '$_domain' saved to configuration for future use." - - return 0 - fi - - _debug "API check failed for '$_cutdomain'. Continuing search." - - p=$i - i=$((i + 1)) - done - - _err "Could not detect root zone for '$domain'. Please set SPACESHIP_ROOT_DOMAIN manually." - return 1 -} - -_spaceship_api_request() { - method="$1" - url="$2" - payload="$3" - - _debug2 "Sending $method request to $url with payload $payload" - if [ "$method" = "GET" ]; then - response="$(_get "$url")" - else - response="$(_post "$payload" "$url" "" "$method")" - fi - - if [ "$?" != "0" ]; then - _err "API request failed. Response: $response" - return 1 - fi - - _debug2 "API response body: $response" - - if [ "$method" = "GET" ]; then - if _contains "$(_head_n 1 <"$HTTP_HEADER")" '200'; then - return 0 - fi - else - if _contains "$(_head_n 1 <"$HTTP_HEADER")" '204'; then - return 0 - fi - fi - - _debug2 "API response header: $HTTP_HEADER" - return 1 -} diff --git a/dnsapi/dns_subreg.sh b/dnsapi/dns_subreg.sh deleted file mode 100644 index 5e7e7ced..00000000 --- a/dnsapi/dns_subreg.sh +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_subreg_info='Subreg.cz -Site: subreg.cz -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_subreg -Options: - SUBREG_API_USERNAME API username - SUBREG_API_PASSWORD API password -Issues: github.com/acmesh-official/acme.sh/issues/6835 -Author: Tomas Pavlic -' - -# Subreg SOAP API -# https://subreg.cz/manual/ - -SUBREG_API_URL="https://soap.subreg.cz/cmd.php" - -######## Public functions ##################### - -# Usage: dns_subreg_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_subreg_add() { - fulldomain=$1 - txtvalue=$2 - - SUBREG_API_USERNAME="${SUBREG_API_USERNAME:-$(_readaccountconf_mutable SUBREG_API_USERNAME)}" - SUBREG_API_PASSWORD="${SUBREG_API_PASSWORD:-$(_readaccountconf_mutable SUBREG_API_PASSWORD)}" - if [ -z "$SUBREG_API_USERNAME" ] || [ -z "$SUBREG_API_PASSWORD" ]; then - _err "SUBREG_API_USERNAME and SUBREG_API_PASSWORD are not set." - return 1 - fi - - _saveaccountconf_mutable SUBREG_API_USERNAME "$SUBREG_API_USERNAME" - _saveaccountconf_mutable SUBREG_API_PASSWORD "$SUBREG_API_PASSWORD" - - if ! _subreg_login; then - return 1 - fi - - if ! _get_root "$fulldomain"; then - _err "Cannot determine root domain for: $fulldomain" - return 1 - fi - - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _subreg_soap "Add_DNS_Record" "$_domain$_sub_domainTXT$txtvalue0120" - if _subreg_ok; then - _record_id="$(_subreg_map_get record_id)" - - if [ -z "$_record_id" ]; then - _err "Subreg API did not return a record_id for TXT record on $fulldomain" - _err "$response" - return 1 - fi - - _savedomainconf "$(_subreg_record_id_key "$txtvalue")" "$_record_id" - return 0 - fi - _err "Failed to add TXT record." - _err "$response" - return 1 -} - -# Usage: dns_subreg_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_subreg_rm() { - fulldomain=$1 - txtvalue=$2 - - SUBREG_API_USERNAME="${SUBREG_API_USERNAME:-$(_readaccountconf_mutable SUBREG_API_USERNAME)}" - SUBREG_API_PASSWORD="${SUBREG_API_PASSWORD:-$(_readaccountconf_mutable SUBREG_API_PASSWORD)}" - if [ -z "$SUBREG_API_USERNAME" ] || [ -z "$SUBREG_API_PASSWORD" ]; then - _err "SUBREG_API_USERNAME and SUBREG_API_PASSWORD are not set." - return 1 - fi - - if ! _subreg_login; then - return 1 - fi - - if ! _get_root "$fulldomain"; then - _err "Cannot determine root domain for: $fulldomain" - return 1 - fi - - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _record_id="$(_readdomainconf "$(_subreg_record_id_key "$txtvalue")")" - if [ -z "$_record_id" ]; then - _err "Could not find saved record ID for $fulldomain" - return 1 - fi - - _debug "Deleting record ID: $_record_id" - _subreg_soap "Delete_DNS_Record" "$_domain$_record_id" - if _subreg_ok; then - - _cleardomainconf "$(_subreg_record_id_key "$txtvalue")" - return 0 - fi - - _err "Failed to delete TXT record." - _err "$response" - return 1 -} - -#################### Private functions ##################### - -# Build a domain-conf key for storing the record ID of a given TXT value. -# Base64url chars include '-' which is invalid in shell variable names, so replace with '_'. -_subreg_record_id_key() { - printf 'SUBREG_RECORD_ID_%s' "$(printf '%s' "$1" | tr '-' '_')" -} - -# Check if the current $response contains a successful status in the ns2:Map format: -# statusok -_subreg_ok() { - [ "$(_subreg_map_get status)" = "ok" ] -} - -# Extract the value for a given key from the ns2:Map response. -# Usage: _subreg_map_get keyname -# Reads from $response -_subreg_map_get() { - _key="$1" - echo "$response" | tr -d '\n\r' | _egrep_o ">${_key}]*>[^<]*" | sed 's/.*]*>//;s/<\/value>//' -} - -# Login and store session token in _subreg_ssid -_subreg_login() { - _debug "Logging in to Subreg API as $SUBREG_API_USERNAME" - _subreg_soap_noauth "Login" "$SUBREG_API_USERNAME$SUBREG_API_PASSWORD" - if ! _subreg_ok; then - _err "Subreg login failed." - _err "$response" - return 1 - fi - _subreg_ssid="$(_subreg_map_get ssid)" - if [ -z "$_subreg_ssid" ]; then - _err "Subreg login: could not extract session token (ssid)." - return 1 - fi - _debug "Subreg login: session token (ssid) obtained" - return 0 -} - -# _get_root _acme-challenge.www.domain.com -# returns _sub_domain and _domain -_get_root() { - domain=$1 - i=1 - p=1 - - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - if [ -z "$h" ]; then - _err "Unable to retrieve DNS zone matching domain: $domain" - return 1 - fi - - _subreg_soap "Get_DNS_Zone" "$h" - - if _subreg_ok; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain="$h" - return 0 - fi - - p=$i - i=$(_math "$i" + 1) - done -} - -# Send a SOAP request without authentication (used for Login) -# _subreg_soap_noauth command inner_xml -_subreg_build_soap() { - _cmd="$1" - _data_inner="$2" - - _soap_body=" - - - - - ${_data_inner} - - - -" - - export _H1="Content-Type: text/xml" - export _H2="SOAPAction: http://soap.subreg.cz/soap#${_cmd}" - response="$(_post "$_soap_body" "$SUBREG_API_URL" "" "POST" "text/xml")" -} - -# Send an authenticated SOAP request (requires _subreg_ssid to be set) -# _subreg_soap command inner_xml -_subreg_soap_noauth() { - _cmd="$1" - _inner="$2" - - _subreg_build_soap "$_cmd" "$_inner" -} - -# Send an authenticated SOAP request (requires _subreg_ssid to be set) -# _subreg_soap command inner_xml -_subreg_soap() { - _cmd="$1" - _inner="$2" - _inner_with_ssid="${_subreg_ssid}${_inner}" - - _subreg_build_soap "$_cmd" "$_inner_with_ssid" -} diff --git a/dnsapi/dns_technitium.sh b/dnsapi/dns_technitium.sh index fbe44606..7bc0dd48 100755 --- a/dnsapi/dns_technitium.sh +++ b/dnsapi/dns_technitium.sh @@ -6,7 +6,6 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_technitium Options: Technitium_Server Server Address Technitium_Token API Token - Technitium_Expiry_Ttl Number of seconds before DNS server auto-deletes the acme record Issues: github.com/acmesh-official/acme.sh/issues/6116 Author: Henning Reich ' @@ -16,10 +15,7 @@ dns_technitium_add() { _Technitium_account fulldomain=$1 txtvalue=$2 - expiryTtl=${Technitium_Expirty_Ttl:-$(_readaccountconf_mutable Technitium_Expiry_Ttl)} - expiryTtl=${expiryTtl:-0} - - response="$(_get "$Technitium_Server/api/zones/records/add?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}&expiryTtl=$expiryTtl")" + response="$(_get "$Technitium_Server/api/zones/records/add?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" if _contains "$response" '"status":"ok"'; then return 0 fi @@ -32,14 +28,6 @@ dns_technitium_rm() { _Technitium_account fulldomain=$1 txtvalue=$2 - expiryTtl=${Technitium_Expirty_Ttl:-$(_readaccountconf_mutable Technitium_Expiry_Ttl)} - expiryTtl=${expiryTtl:-0} - - if [ "$expiryTtl" -ne 0 ]; then - _info "DNS record is configured to be auto-removed after $expiryTtl seconds. Remove operation is bypassed." - return 0 - fi - response="$(_get "$Technitium_Server/api/zones/records/delete?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" if _contains "$response" '"status":"ok"'; then return 0 diff --git a/dnsapi/dns_tele3.sh b/dnsapi/dns_tele3.sh index 3a3ccf8c..e5974951 100644 --- a/dnsapi/dns_tele3.sh +++ b/dnsapi/dns_tele3.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#tele3 Options: TELE3_Key API Key TELE3_Secret API Secret -Author: Roman Blizik <@par-pa> +Author: Roman Blizik ' TELE3_API="https://www.tele3.cz/acme/" diff --git a/dnsapi/dns_tencent.sh b/dnsapi/dns_tencent.sh index b148adc3..d82768b9 100644 --- a/dnsapi/dns_tencent.sh +++ b/dnsapi/dns_tencent.sh @@ -2,7 +2,7 @@ # shellcheck disable=SC2034 dns_tencent_info='Tencent.com Site: cloud.Tencent.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_tencent +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_tencent Options: Tencent_SecretId Secret ID Tencent_SecretKey Secret Key diff --git a/dnsapi/dns_timeweb.sh b/dnsapi/dns_timeweb.sh index 7040ac9a..544564ea 100644 --- a/dnsapi/dns_timeweb.sh +++ b/dnsapi/dns_timeweb.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_timeweb Options: TW_Token API JWT token. Get it from the control panel at https://timeweb.cloud/my/api-keys Issues: github.com/acmesh-official/acme.sh/issues/5140 -Author: Nikolay Pronchev <@nikolaypronchev> +Author: Nikolay Pronchev ' TW_Api="https://api.timeweb.cloud/api/v1" diff --git a/dnsapi/dns_transip.sh b/dnsapi/dns_transip.sh index b3c5ed70..2abbe34d 100644 --- a/dnsapi/dns_transip.sh +++ b/dnsapi/dns_transip.sh @@ -24,7 +24,7 @@ dns_transip_add() { _debug txtvalue="$txtvalue" _transip_setup "$fulldomain" || return 1 _info "Creating TXT record." - if ! _transip_rest POST "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":60}}"; then + if ! _transip_rest POST "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":300}}"; then _err "Could not add TXT record." return 1 fi @@ -38,7 +38,7 @@ dns_transip_rm() { _debug txtvalue="$txtvalue" _transip_setup "$fulldomain" || return 1 _info "Removing TXT record." - if ! _transip_rest DELETE "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":60}}"; then + if ! _transip_rest DELETE "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":300}}"; then _err "Could not remove TXT record $_sub_domain for $domain" return 1 fi diff --git a/dnsapi/dns_udr.sh b/dnsapi/dns_udr.sh index dbc959d6..f9772e10 100644 --- a/dnsapi/dns_udr.sh +++ b/dnsapi/dns_udr.sh @@ -7,7 +7,7 @@ Options: UDR_USER Username UDR_PASS Password Issues: github.com/acmesh-official/acme.sh/issues/3923 -Author: Andreas Scherer <@andischerer> +Author: Andreas Scherer ' UDR_API="https://api.domainreselling.de/api/call.cgi" @@ -145,8 +145,8 @@ _udr_rest() { _debug data "${data}" response="$(_post "${data}" "${UDR_API}?s_login=${UDR_USER}&s_pw=${UDR_PASS}" "" "POST")" - _code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | tr -d ' \t\r') - _description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | tr -d '\r' | sed -e 's/^[ ]*//' -e 's/[ ]*$//') + _code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + _description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') _debug response_code "$_code" _debug response_description "$_description" diff --git a/dnsapi/dns_variomedia.sh b/dnsapi/dns_variomedia.sh index 4620b854..fa38bbb6 100644 --- a/dnsapi/dns_variomedia.sh +++ b/dnsapi/dns_variomedia.sh @@ -74,7 +74,7 @@ dns_variomedia_rm() { return 1 fi - _record_id="$(echo "$response" | sed -E 's/,"tags":\[[^]]*\]//g' | cut -d '[' -f3 | cut -d']' -f1 | sed 's/},[ \t]*{/\},§\{/g' | tr § '\n' | grep -i "$_sub_domain" | grep -- "$txtvalue" | sed 's/^{//;s/}[,]?$//' | tr , '\n' | tr -d '\"' | grep ^id | cut -d : -f2 | tr -d ' ')" + _record_id="$(echo "$response" | sed -E 's/,"tags":\[[^]]*\]//g' | cut -d '[' -f2 | cut -d']' -f1 | sed 's/},[ \t]*{/\},§\{/g' | tr § '\n' | grep "$_sub_domain" | grep -- "$txtvalue" | sed 's/^{//;s/}[,]?$//' | tr , '\n' | tr -d '\"' | grep ^id | cut -d : -f2 | tr -d ' ')" _debug _record_id "$_record_id" if [ "$_record_id" ]; then _info "Successfully retrieved the record id for ACME challenge." diff --git a/dnsapi/dns_virakcloud.sh b/dnsapi/dns_virakcloud.sh deleted file mode 100755 index 7ae665d2..00000000 --- a/dnsapi/dns_virakcloud.sh +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_virakcloud_info='VirakCloud DNS API -Site: VirakCloud.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_virakcloud -Options: - VIRAKCLOUD_API_TOKEN VirakCloud API Bearer Token -' - -VIRAKCLOUD_API_URL="https://public-api.virakcloud.com/dns" - -######## Public functions ##################### - -#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -#Used to add txt record -dns_virakcloud_add() { - fulldomain=$1 - txtvalue=$2 - - VIRAKCLOUD_API_TOKEN="${VIRAKCLOUD_API_TOKEN:-$(_readaccountconf_mutable VIRAKCLOUD_API_TOKEN)}" - - if [ -z "$VIRAKCLOUD_API_TOKEN" ]; then - _err "You haven't configured your VirakCloud API token yet." - _err "Please set VIRAKCLOUD_API_TOKEN environment variable or run:" - _err " export VIRAKCLOUD_API_TOKEN=\"your-api-token\"" - return 1 - fi - - _saveaccountconf_mutable VIRAKCLOUD_API_TOKEN "$VIRAKCLOUD_API_TOKEN" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" - if [ "$http_code" = "401" ]; then - return 1 - fi - _err "Invalid domain" - return 1 - fi - - _debug _domain "$_domain" - _debug fulldomain "$fulldomain" - - _info "Adding TXT record" - - if _virakcloud_rest POST "domains/${_domain}/records" "{\"record\":\"${fulldomain}\",\"type\":\"TXT\",\"ttl\":3600,\"content\":\"${txtvalue}\"}"; then - if echo "$response" | grep -q "success" || echo "$response" | grep -q "\"data\""; then - _info "Added, OK" - return 0 - elif echo "$response" | grep -q "already exists" || echo "$response" | grep -q "duplicate"; then - _info "Record already exists, OK" - return 0 - else - _err "Add TXT record error." - _err "Response: $response" - return 1 - fi - fi - - _err "Add TXT record error." - return 1 -} - -#Usage: fulldomain txtvalue -#Used to remove the txt record after validation -dns_virakcloud_rm() { - fulldomain=$1 - txtvalue=$2 - - VIRAKCLOUD_API_TOKEN="${VIRAKCLOUD_API_TOKEN:-$(_readaccountconf_mutable VIRAKCLOUD_API_TOKEN)}" - - if [ -z "$VIRAKCLOUD_API_TOKEN" ]; then - _err "You haven't configured your VirakCloud API token yet." - return 1 - fi - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" - if [ "$http_code" = "401" ]; then - return 1 - fi - _err "Invalid domain" - return 1 - fi - - _debug _domain "$_domain" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - - _info "Removing TXT record" - - _debug "Getting list of records to find content ID" - if ! _virakcloud_rest GET "domains/${_domain}/records" ""; then - return 1 - fi - - _debug2 "Records response" "$response" - - contentid="" - # Extract innermost objects (content objects) which look like {"id":"...","content_raw":"..."} - # We filter for the one containing txtvalue - - target_obj=$(echo "$response" | grep -o '{[^}]*}' | grep "$txtvalue" | _head_n 1) - - if [ -n "$target_obj" ]; then - contentid=$(echo "$target_obj" | _egrep_o '"id":"[^"]*"' | cut -d '"' -f 4) - fi - - if [ -z "$contentid" ]; then - _debug "Could not find matching record ID in response" - _info "Record not found, may have been already removed" - return 0 - fi - - _debug contentid "$contentid" - - if _virakcloud_rest DELETE "domains/${_domain}/records/${fulldomain}/TXT/${contentid}" ""; then - if echo "$response" | grep -q "success" || [ -z "$response" ]; then - _info "Removed, OK" - return 0 - elif echo "$response" | grep -q "not found" || echo "$response" | grep -q "404"; then - _info "Record not found, OK" - return 0 - else - _err "Remove TXT record error." - _err "Response: $response" - return 1 - fi - fi - - _err "Remove TXT record error." - return 1 -} - -#################### Private functions below ################################## - -#_acme-challenge.www.domain.com -#returns -# _domain=domain.com -_get_root() { - domain=$1 - i=1 - p=1 - - # Optimization: skip _acme-challenge subdomain to avoid 422 errors - if echo "$domain" | grep -q "^_acme-challenge."; then - i=2 - fi - - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug h "$h" - - if [ -z "$h" ]; then - return 1 - fi - - if ! _virakcloud_rest GET "domains/$h" ""; then - http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" - if [ "$http_code" = "401" ]; then - return 1 - fi - p=$i - i=$(_math "$i" + 1) - continue - fi - - if echo "$response" | grep -q "\"name\""; then - _domain="$h" - return 0 - fi - - p=$i - i=$(_math "$i" + 1) - done - - return 1 -} - -_virakcloud_rest() { - m=$1 - ep="$2" - data="$3" - - _debug "$ep" - - export _H1="Content-Type: application/json" - export _H2="Authorization: Bearer $VIRAKCLOUD_API_TOKEN" - - if [ "$m" != "GET" ]; then - _debug data "$data" - response="$(_post "$data" "$VIRAKCLOUD_API_URL/$ep" "" "$m")" - else - response="$(_get "$VIRAKCLOUD_API_URL/$ep")" - fi - - _ret="$?" - - if [ "$_ret" != "0" ]; then - _err "error on $m $ep" - return 1 - fi - - http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" - _debug "http response code" "$http_code" - - if [ "$http_code" = "401" ]; then - _err "VirakCloud API returned 401 Unauthorized." - _err "Your VIRAKCLOUD_API_TOKEN is invalid or expired." - _err "Please check your API token and try again." - return 1 - fi - - if [ "$http_code" = "403" ]; then - _err "VirakCloud API returned 403 Forbidden." - _err "Your API token does not have permission to access this resource." - return 1 - fi - - if [ -n "$http_code" ] && [ "$http_code" -ge 400 ]; then - _err "VirakCloud API error. HTTP code: $http_code" - _err "Response: $response" - return 1 - fi - - _debug2 response "$response" - return 0 -} diff --git a/dnsapi/dns_volcengine.sh b/dnsapi/dns_volcengine.sh deleted file mode 100755 index 2cc805d5..00000000 --- a/dnsapi/dns_volcengine.sh +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_volcengine_info='Volcano Engine DNS API -Site: https://www.volcengine.com/docs/6758/155086 -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_volcengine -Options: - Volcengine_ACCESS_KEY_ID API Key ID - Volcengine_SECRET_ACCESS_KEY API Secret - Volcengine_SESSION_TOKEN Session Token. Optional, only needed when using temporary STS credentials. -Issues: github.com/acmesh-official/acme.sh/issues/7064 -' - -Volcengine_HOST="dns.volcengineapi.com" -Volcengine_URL="https://$Volcengine_HOST" - -######## Public functions ##################### - -#fulldomain txtvalue -dns_volcengine_add() { - fulldomain=$1 - txtvalue=$2 - _record_id="" - - Volcengine_ACCESS_KEY_ID="${Volcengine_ACCESS_KEY_ID:-$(_readaccountconf_mutable Volcengine_ACCESS_KEY_ID)}" - Volcengine_SECRET_ACCESS_KEY="${Volcengine_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable Volcengine_SECRET_ACCESS_KEY)}" - - if [ -z "$Volcengine_ACCESS_KEY_ID" ] || [ -z "$Volcengine_SECRET_ACCESS_KEY" ]; then - Volcengine_ACCESS_KEY_ID="" - Volcengine_SECRET_ACCESS_KEY="" - _err "You haven't specified the volcengine dns api key id and api key secret yet." - return 1 - fi - - #save the api key and email to the account conf file. - _saveaccountconf_mutable Volcengine_ACCESS_KEY_ID "$Volcengine_ACCESS_KEY_ID" - _saveaccountconf_mutable Volcengine_SECRET_ACCESS_KEY "$Volcengine_SECRET_ACCESS_KEY" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - _sleep 1 - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - # _info "Getting existing records for $fulldomain" - if ! volcengine_rest POST "" "Action=ListRecords&Version=2018-08-01" "{\"ZID\":$_domain_id,\"Host\":\"$_sub_domain\",\"Type\":\"TXT\",\"Value\":\"$txtvalue\",\"SearchMode\":\"exact\"}"; then - _sleep 1 - return 1 - fi - - # ListRecords already filtered by ZID + Host + Value + SearchMode:exact, - # so any returned record is our target. Don't match on FQDN: Volcengine - # lowercases the Host/FQDN in the response, which would break a - # case-sensitive string compare against $fulldomain. - _record_id="$(echo "$response" | _egrep_o "\"RecordID\":\"[0-9]+\"," | cut -d: -f2 | cut -d, -f1 | tr -d '"')" - _debug "_record_id" "$_record_id" - - if [ "$_record_id" ] && _contains "$response" "$txtvalue"; then - _info "The TXT record already exists. Skipping." - _sleep 1 - return 0 - fi - - _debug "Adding records" - - if volcengine_rest POST "" "Action=CreateRecord&Version=2018-08-01" "{\"ZID\":$_domain_id,\"Host\":\"$_sub_domain\",\"Type\":\"TXT\",\"Value\":\"$txtvalue\",\"Remark\":\"acme.sh\"}"; then - _info "TXT record updated successfully." - _sleep 1 - return 0 - fi - - _sleep 1 - return 1 -} - -#fulldomain txtvalue -dns_volcengine_rm() { - fulldomain=$1 - txtvalue=$2 - _record_id="" - - Volcengine_ACCESS_KEY_ID="${Volcengine_ACCESS_KEY_ID:-$(_readaccountconf_mutable Volcengine_ACCESS_KEY_ID)}" - Volcengine_SECRET_ACCESS_KEY="${Volcengine_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable Volcengine_SECRET_ACCESS_KEY)}" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - _sleep 1 - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _info "Getting existing records for $fulldomain" - - if ! volcengine_rest POST "" "Action=ListRecords&Version=2018-08-01" "{\"ZID\":$_domain_id,\"Host\":\"$_sub_domain\",\"Type\":\"TXT\",\"Value\":\"$txtvalue\",\"SearchMode\":\"exact\"}"; then - _sleep 1 - return 1 - fi - - # ListRecords already filtered by ZID + Host + Value + SearchMode:exact, - # so any returned record is our target. Don't match on FQDN: Volcengine - # lowercases the Host/FQDN in the response, which would break a - # case-sensitive string compare against $fulldomain. - _record_id="$(echo "$response" | _egrep_o "\"RecordID\":\"[0-9]+\"," | cut -d: -f2 | cut -d, -f1 | tr -d '"')" - _debug "_record_id" "$_record_id" - - if [ -z "$_record_id" ]; then - _debug "no records exist, skip" - _sleep 1 - return 0 - fi - - if volcengine_rest POST "" "Action=DeleteRecord&Version=2018-08-01" "{\"RecordID\":\"$_record_id\"}"; then - _info "TXT record deleted successfully." - _sleep 1 - return 0 - fi - _sleep 1 - return 1 -} - -#################### Private functions below ################################## - -_get_root() { - domain=$1 - i=1 - p=1 - - # 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) - _debug "Checking domain: $h" - if [ -z "$h" ]; then - _err "invalid domain" - return 1 - fi - - # iterate over paginated result for list_hosted_zones - if ! volcengine_rest POST "" "Action=ListZones&Version=2018-08-01" "{\"Key\":\"$h\",\"SearchMode\":\"exact\"}"; then - return 1 - fi - if _contains "$response" "\"ZoneName\":\"$h\""; then - _domain_id=$(printf "%s" "$response" | _egrep_o "\"ZID\":[0-9]+," | cut -d: -f2 | cut -d, -f1) - if [ "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - return 0 - fi - _err "Can't find domain with id: $h" - return 1 - fi - p=$i - i=$(_math "$i" + 1) - done - return 1 -} - -#method uri qstr data -volcengine_rest() { - mtd="$1" - ep="$2" - qsr="$3" - data="$4" - - _debug mtd "$mtd" - _debug ep "$ep" - _debug qsr "$qsr" - _debug data "$data" - - # clear any header state left over from a previous request so that - # conditionally-set headers (e.g. x-content-sha256, x-security-token) - # can't leak into the next request - _H1="" - _H2="" - _H3="" - _H4="" - _H5="" - - CanonicalURI="/$ep" - _debug2 CanonicalURI "$CanonicalURI" - - CanonicalQueryString="$qsr" - _debug2 CanonicalQueryString "$CanonicalQueryString" - - RequestDate="$(date -u +"%Y%m%dT%H%M%SZ")" - _debug2 RequestDate "$RequestDate" - - Hash="sha256" - - _H1="X-Date: $RequestDate" - _debug2 _H1 "$_H1" - - volcengine_host="$Volcengine_HOST" - CanonicalHeaders="host:$volcengine_host\n" - SignedHeaders="host" - - if [ -n "$data" ]; then - XContentSha256="$(printf "%s" "$data" | _digest "$Hash" hex)" - _H4="x-content-sha256: $XContentSha256" - _debug2 _H4 "$_H4" - - CanonicalHeaders="${CanonicalHeaders}x-content-sha256:$XContentSha256\n" - SignedHeaders="${SignedHeaders};x-content-sha256" - fi - - CanonicalHeaders="${CanonicalHeaders}x-date:$RequestDate\n" - SignedHeaders="${SignedHeaders};x-date" - - if [ -n "$Volcengine_SESSION_TOKEN" ]; then - _H3="x-security-token: $Volcengine_SESSION_TOKEN" - CanonicalHeaders="${CanonicalHeaders}x-security-token:$Volcengine_SESSION_TOKEN\n" - SignedHeaders="${SignedHeaders};x-security-token" - fi - - _debug2 CanonicalHeaders "$CanonicalHeaders" - _debug2 SignedHeaders "$SignedHeaders" - - RequestPayload="$data" - _debug2 RequestPayload "$RequestPayload" - - CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$(printf "%s" "$RequestPayload" | _digest "$Hash" hex)" - _debug2 CanonicalRequest "$CanonicalRequest" - - HashedCanonicalRequest="$(printf '%b' "$CanonicalRequest" | _digest "$Hash" hex)" - _debug2 HashedCanonicalRequest "$HashedCanonicalRequest" - - Algorithm="HMAC-SHA256" - _debug2 Algorithm "$Algorithm" - - RequestDateOnly="$(echo "$RequestDate" | cut -c 1-8)" - _debug2 RequestDateOnly "$RequestDateOnly" - - Region="cn-beijing" - Service="dns" - - CredentialScope="$RequestDateOnly/$Region/$Service/request" - _debug2 CredentialScope "$CredentialScope" - - StringToSign="$Algorithm\n$RequestDate\n$CredentialScope\n$HashedCanonicalRequest" - - _debug2 StringToSign "$StringToSign" - - kSecret="$Volcengine_SECRET_ACCESS_KEY" - - _secure_debug2 kSecret "$kSecret" - - kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")" - _secure_debug2 kSecretH "$kSecretH" - - kDateH="$(printf "%s" "$RequestDateOnly" | _hmac "$Hash" "$kSecretH" hex)" - _debug2 kDateH "$kDateH" - - kRegionH="$(printf "%s" "$Region" | _hmac "$Hash" "$kDateH" hex)" - _debug2 kRegionH "$kRegionH" - - kServiceH="$(printf "%s" "$Service" | _hmac "$Hash" "$kRegionH" hex)" - _debug2 kServiceH "$kServiceH" - - kSigningH="$(printf "%s" "request" | _hmac "$Hash" "$kServiceH" hex)" - _debug2 kSigningH "$kSigningH" - - signature="$(printf '%b' "$StringToSign" | _hmac "$Hash" "$kSigningH" hex)" - _debug2 signature "$signature" - - Authorization="$Algorithm Credential=$Volcengine_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature" - _debug2 Authorization "$Authorization" - - _H2="Authorization: $Authorization" - _debug2 _H2 "$_H2" - - url="$Volcengine_URL/$ep" - if [ "$qsr" ]; then - url="$Volcengine_URL/$ep?$qsr" - fi - - if [ "$mtd" = "GET" ]; then - response="$(_get "$url")" - else - response="$(_post "$data" "$url" "" "POST" "application/json")" - fi - - _ret="$?" - _debug response "$response" - if [ "$_ret" = "0" ]; then - if _contains "$response" "\"Error\":{"; then - _err "Response error:$response" - return 1 - fi - fi - - return "$_ret" -} diff --git a/dnsapi/dns_vscale.sh b/dnsapi/dns_vscale.sh index faf3105d..c3915c69 100755 --- a/dnsapi/dns_vscale.sh +++ b/dnsapi/dns_vscale.sh @@ -5,7 +5,7 @@ Site: vscale.io Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_vscale Options: VSCALE_API_KEY API Key -Author: Alex Loban <@LAV45> +Author: Alex Loban ' VSCALE_API_URL="https://api.vscale.io/v1" diff --git a/dnsapi/dns_vultr.sh b/dnsapi/dns_vultr.sh index 4002e5de..61ec3f60 100644 --- a/dnsapi/dns_vultr.sh +++ b/dnsapi/dns_vultr.sh @@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_vultr Options: VULTR_API_KEY API Key Issues: github.com/acmesh-official/acme.sh/issues/2374 +Author: ' VULTR_Api="https://api.vultr.com/v2" diff --git a/dnsapi/dns_websupport.sh b/dnsapi/dns_websupport.sh index 2374afc3..bfc4b23a 100644 --- a/dnsapi/dns_websupport.sh +++ b/dnsapi/dns_websupport.sh @@ -7,7 +7,7 @@ Options: WS_ApiKey API Key. Called "Identifier" in the WS Admin WS_ApiSecret API Secret. Called "Secret key" in the WS Admin Issues: github.com/acmesh-official/acme.sh/issues/3486 -Author: trgo.sk <@trgosk>, @akulumbeg +Author: trgo.sk , akulumbeg ' # Requirements: API Key and Secret from https://admin.websupport.sk/en/auth/apiKey diff --git a/dnsapi/dns_wedos.sh b/dnsapi/dns_wedos.sh deleted file mode 100644 index d1f353e2..00000000 --- a/dnsapi/dns_wedos.sh +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_wedos_info='WEDOS.com -Site: wedos.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_wedos -Options: - WEDOS_Username WAPI login (account email) - WEDOS_Wapipass WAPI password -Issues: github.com/acmesh-official/acme.sh/issues/7071 -Author: Jan Forman -' - -WEDOS_Api="https://api.wedos.com/wapi/json" - -######## Public functions ##################### - -#Usage: dns_wedos_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_wedos_add() { - fulldomain=$(echo "$1" | _lower_case) - txtvalue=$2 - - if ! _wedos_init; then - return 1 - fi - - _debug "Detecting root zone for $fulldomain" - if ! _get_root "$fulldomain"; then - _err "Cannot determine root zone for: $fulldomain" - return 1 - fi - _debug _domain "$_domain" - _debug _sub_domain "$_sub_domain" - - _info "Adding TXT record: $_sub_domain.$_domain" - if ! _wedos_request "dns-row-add" "{\"domain\":\"$_domain\",\"name\":\"$_sub_domain\",\"ttl\":\"300\",\"type\":\"TXT\",\"rdata\":\"$txtvalue\"}"; then - _err "Failed to add TXT record" - return 1 - fi - - _info "Committing DNS changes for $_domain" - if ! _wedos_request "dns-domain-commit" "{\"name\":\"$_domain\"}"; then - _err "Failed to commit DNS changes" - return 1 - fi - - return 0 -} - -#Usage: dns_wedos_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_wedos_rm() { - fulldomain=$(echo "$1" | _lower_case) - txtvalue=$2 - - if ! _wedos_init; then - return 1 - fi - - _debug "Detecting root zone for $fulldomain" - if ! _get_root "$fulldomain"; then - _err "Cannot determine root zone for: $fulldomain" - return 1 - fi - _debug _domain "$_domain" - _debug _sub_domain "$_sub_domain" - - # _get_root leaves the dns-rows-list response for $_domain in $response - _debug "Looking up row IDs for TXT value: $txtvalue" - _row_ids=$(echo "$response" | tr '{' '\n' | grep -F -- "\"rdata\":\"$txtvalue\"" | grep -F -- "\"name\":\"$_sub_domain\"" | _egrep_o '"ID": *"[0-9]*"' | tr -dc '0-9\n') - _debug _row_ids "$_row_ids" - - if [ -z "$_row_ids" ]; then - _info "TXT record not found, nothing to remove" - return 0 - fi - - for _row_id in $_row_ids; do - _info "Removing TXT record ID $_row_id from $_domain" - if ! _wedos_request "dns-row-delete" "{\"domain\":\"$_domain\",\"row_id\":\"$_row_id\"}"; then - _err "Failed to delete TXT record" - return 1 - fi - done - - _info "Committing DNS changes for $_domain" - if ! _wedos_request "dns-domain-commit" "{\"name\":\"$_domain\"}"; then - _err "Failed to commit DNS changes" - return 1 - fi - - return 0 -} - -#################### Private functions below ################################## - -_wedos_init() { - WEDOS_Username="${WEDOS_Username:-$(_readaccountconf_mutable WEDOS_Username)}" - WEDOS_Wapipass="${WEDOS_Wapipass:-$(_readaccountconf_mutable WEDOS_Wapipass)}" - - if [ -z "$WEDOS_Username" ] || [ -z "$WEDOS_Wapipass" ]; then - WEDOS_Username="" - WEDOS_Wapipass="" - _err "You didn't specify the WEDOS WAPI credentials yet." - _err "Please export WEDOS_Username and WEDOS_Wapipass and try again." - return 1 - fi - - _saveaccountconf_mutable WEDOS_Username "$WEDOS_Username" - _saveaccountconf_mutable WEDOS_Wapipass "$WEDOS_Wapipass" - return 0 -} - -# WAPI auth token: sha1(login + sha1(password) + hour), where the hour is -# the current hour on the WEDOS servers (Europe/Prague timezone). -# The POSIX TZ string is used so no tzdata is required on the client. -_wedos_auth() { - if [ "$_wedos_utc" ]; then - # fallback: WAPI accepts 1 hour of skew, UTC+1 fits both CET and CEST - _wedos_hour=$(date -u +%H) - _wedos_hour=$(printf '%02d' "$(((${_wedos_hour#0} + 1) % 24))") - else - _wedos_hour=$(TZ='CET-1CEST,M3.5.0,M10.5.0/3' date +%H) - fi - _wedos_phash=$(printf '%s' "$WEDOS_Wapipass" | _digest sha1 hex) - printf '%s' "${WEDOS_Username}${_wedos_phash}${_wedos_hour}" | _digest sha1 hex -} - -#Usage: _wedos_request -#Returns 0 and sets $response on WAPI code 1000, returns 1 otherwise. -_wedos_request() { - _wedos_cmd="$1" - _wedos_data="$2" - - _wedos_token=$(_wedos_auth) - _secure_debug _wedos_token "$_wedos_token" - - _wedos_json="{\"request\":{\"user\":\"$WEDOS_Username\",\"auth\":\"$_wedos_token\",\"command\":\"$_wedos_cmd\",\"data\":$_wedos_data}}" - _debug2 "WAPI command: $_wedos_cmd" - _debug2 "WAPI data: $_wedos_data" - - # _post sends the global _H1.._H5 headers with every request; clear them so - # headers from earlier API calls are not leaked to the WAPI endpoint. - export _H1="" - export _H2="" - export _H3="" - export _H4="" - export _H5="" - - _wedos_body="request=$(printf '%s' "$_wedos_json" | _url_encode)" - response=$(_post "$_wedos_body" "$WEDOS_Api" "" "POST" "application/x-www-form-urlencoded") - if [ "$?" != "0" ]; then - _err "WAPI request failed for command '$_wedos_cmd'" - return 1 - fi - _debug2 "WAPI response: $response" - - _wedos_code=$(echo "$response" | _egrep_o '"code": *[0-9]*' | _head_n 1 | tr -dc '0-9') - _debug2 "WAPI result code: $_wedos_code" - if [ "$_wedos_code" = "1000" ]; then - return 0 - fi - - # some systems ignore the TZ variable (Haiku), sending a wrong auth hour; - # retry once with the UTC fallback in _wedos_auth - if [ "$_wedos_code" = "2050" ] && [ -z "$_wedos_utc" ]; then - _wedos_utc=1 - _wedos_request "$_wedos_cmd" "$_wedos_data" - return $? - fi - - # 2050 = bad credentials, 2051 = IP not whitelisted, 2052 = IP blocked - if [ "$_wedos_code" = "2050" ] || [ "$_wedos_code" = "2051" ] || [ "$_wedos_code" = "2052" ]; then - _wedos_result=$(echo "$response" | _egrep_o '"result": *"[^"]*"' | _head_n 1 | cut -d '"' -f 4) - _err "WAPI authentication error $_wedos_code: $_wedos_result" - _err "Check WEDOS_Username, WEDOS_Wapipass and the WAPI IP whitelist." - _wedos_autherr=1 - return 1 - fi - - _debug "WAPI error for command '$_wedos_cmd': $response" - return 1 -} - -# Determine the registered domain (_domain) and subdomain prefix (_sub_domain) -# by walking up the labels and calling dns-rows-list until WAPI accepts one. -# _acme-challenge.www.example.co.uk -# -> _sub_domain=_acme-challenge.www _domain=example.co.uk -# The full domain itself is tried first, so a zone apex (e.g. DNS alias mode -# pointing at the registered domain) resolves to an empty _sub_domain. -_get_root() { - _gr_full="$1" - _gr_i=1 - _wedos_autherr="" - while true; do - _gr_candidate=$(printf '%s' "$_gr_full" | cut -d . -f "${_gr_i}"-100) - _debug2 "Checking zone candidate: $_gr_candidate" - if [ -z "$_gr_candidate" ]; then - return 1 - fi - - if _wedos_request "dns-rows-list" "{\"domain\":\"$_gr_candidate\"}"; then - _domain="$_gr_candidate" - if [ "$_gr_i" = "1" ]; then - _sub_domain="" - else - _sub_domain=$(printf '%s' "$_gr_full" | cut -d . -f 1-"$((_gr_i - 1))") - fi - return 0 - fi - - # auth error hits every candidate, stop the walk - if [ "$_wedos_autherr" ]; then - return 1 - fi - - _gr_i=$((_gr_i + 1)) - done -} diff --git a/dnsapi/dns_world4you.sh b/dnsapi/dns_world4you.sh index 0a1cda6b..46cdc4fe 100644 --- a/dnsapi/dns_world4you.sh +++ b/dnsapi/dns_world4you.sh @@ -7,7 +7,7 @@ Options: WORLD4YOU_USERNAME Username WORLD4YOU_PASSWORD Password Issues: github.com/acmesh-official/acme.sh/issues/3269 -Author: Lorenz Stechauner <@NerLOR> +Author: Lorenz Stechauner ' WORLD4YOU_API="https://my.world4you.com/en" @@ -61,7 +61,7 @@ dns_world4you_add() { if _contains "$res" "successfully"; then return 0 else - msg=$(_w4y_alert_msg "$res") + msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "]*>[^<]" | sed 's/<[^>]*>//g' | sed 's/^\s*//g') if [ "$msg" = '' ]; then _err "Unable to add record: Unknown error" echo "$ret" >'error-01.html' @@ -110,7 +110,7 @@ dns_world4you_rm() { return 3 fi - recordid=$(echo "$form" | grep 'data-records="' | sed 's/.*"\([^"]*\)".*/\1/;s/"/"/g;s/},{/}\n{/g' | grep '"type":"TXT"' | grep "\"name\":\"$fqdn\"" | grep "\"value\":\"$value\"" | sed 's/^.*"id":"\([^"]*\)".*$/\1/') + recordid=$(printf "TXT:%s.:\"%s\"" "$fqdn" "$value" | _base64) _debug recordid "$recordid" _resethttp @@ -125,7 +125,7 @@ dns_world4you_rm() { if _contains "$res" "successfully"; then return 0 else - msg=$(_w4y_alert_msg "$res") + msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "]*>[^<]" | sed 's/<[^>]*>//g' | sed 's/^\s*//g') if [ "$msg" = '' ]; then _err "Unable to remove record: Unknown error" echo "$ret" >'error-01.html' @@ -145,17 +145,6 @@ dns_world4you_rm() { ################ Private functions ################ -# Usage: _w4y_alert_msg -# Extracts the error text out of the alert box of a DNS page. -# "grep -A" is not portable (Solaris /usr/bin/grep: "illegal option -- A"), -# so select from the alert to EOF and keep the same number of lines. -# "\s" is a GNU sed extension, use an explicit space/tab bracket instead. -_w4y_alert_msg() { - _w4y_tab=$(printf '\t') - echo "$1" | sed -n '/alert-notification/,$p' | _head_n 21 | - grep 'class="weak-title">[^<]' | sed "s/<[^>]*>//g;s/^[ $_w4y_tab]*//" -} - # Usage: _login _login() { WORLD4YOU_USERNAME="${WORLD4YOU_USERNAME:-$(_readaccountconf_mutable WORLD4YOU_USERNAME)}" diff --git a/dnsapi/dns_yandex360.sh b/dnsapi/dns_yandex360.sh index 98841d6b..18d01361 100644 --- a/dnsapi/dns_yandex360.sh +++ b/dnsapi/dns_yandex360.sh @@ -149,7 +149,7 @@ _check_variables() { org_response="$(echo "$org_response" | _normalizeJson)" YANDEX360_ORG_ID=$( echo "$org_response" | - _egrep_o '"id":[ ]*[0-9]+' | + _egrep_o '"id":[[:space:]]*[0-9]+' | cut -d':' -f2 ) _debug 'Automatically retrieved YANDEX360_ORG_ID' "$YANDEX360_ORG_ID" @@ -216,7 +216,7 @@ _get_token() { interval=$( echo "$response" | - _egrep_o '"interval":[ ]*[0-9]+' | + _egrep_o '"interval":[[:space:]]*[0-9]+' | cut -d':' -f2 ) _debug 'Polling interval' "$interval" diff --git a/dnsapi/dns_yc.sh b/dnsapi/dns_yc.sh index cacb5977..36c49ce4 100644 --- a/dnsapi/dns_yc.sh +++ b/dnsapi/dns_yc.sh @@ -22,32 +22,21 @@ dns_yc_add() { fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name txtvalue=$2 - # YC_SA_Key_File_PEM_b64/Path are always persisted to the domain conf below, - # so they must be recovered from there first (account conf is only a - # fallback for the YC_Folder_ID case, see the SA_ID/SA_Key_ID save below). - YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}" YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}" - YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}" YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}" if [ "$YC_SA_Key_File_PEM_b64" ]; then echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key YC_SA_Key_File="private.key" - _yc_key_is_temp=1 _savedomainconf YC_SA_Key_File_PEM_b64 "$YC_SA_Key_File_PEM_b64" else YC_SA_Key_File="$YC_SA_Key_File_Path" - _yc_key_is_temp="" _savedomainconf YC_SA_Key_File_Path "$YC_SA_Key_File_Path" fi - YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}" YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}" - YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}" YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}" - YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}" YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}" - YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}" YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}" if [ "$YC_SA_ID" ] && [ "$YC_SA_Key_ID" ] && [ "$YC_SA_Key_File" ]; then @@ -76,21 +65,11 @@ dns_yc_add() { return 1 fi else - # Clear both possible stores -- YC_Zone_ID/YC_Folder_ID/key material are - # persisted to the domain conf, while YC_SA_ID/YC_SA_Key_ID may have been - # saved account-wide (Folder_ID mode), so a plain _clearaccountconf alone - # would leave stale values behind in whichever store wasn't touched. - _cleardomainconf YC_Zone_ID _clearaccountconf YC_Zone_ID - _cleardomainconf YC_Folder_ID _clearaccountconf YC_Folder_ID - _cleardomainconf YC_SA_ID - _clearaccountconf_mutable YC_SA_ID - _cleardomainconf YC_SA_Key_ID - _clearaccountconf_mutable YC_SA_Key_ID - _cleardomainconf YC_SA_Key_File_PEM_b64 + _clearaccountconf YC_SA_ID + _clearaccountconf YC_SA_Key_ID _clearaccountconf YC_SA_Key_File_PEM_b64 - _cleardomainconf YC_SA_Key_File_Path _clearaccountconf YC_SA_Key_File_Path _err "You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File." return 1 @@ -131,30 +110,11 @@ dns_yc_rm() { fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name txtvalue=$2 - YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}" YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}" - YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}" YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}" - YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}" YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}" - YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}" YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}" - # See dns_yc_add() for why domain conf is checked before account conf. - YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}" - YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}" - YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}" - YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}" - - if [ "$YC_SA_Key_File_PEM_b64" ]; then - echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key - YC_SA_Key_File="private.key" - _yc_key_is_temp=1 - else - YC_SA_Key_File="$YC_SA_Key_File_Path" - _yc_key_is_temp="" - fi - _debug "First detect the root zone" if ! _get_root "$fulldomain"; then _err "invalid domain" @@ -164,10 +124,16 @@ dns_yc_rm() { _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - # upsertRecordSets.deletions removes only the given value from the rrset, - # leaving any other values at the same name (e.g. base + wildcard domain) - # intact -- no need to read the current data set and recompute it. - if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then + _debug "Getting txt records" + if _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then + exists_txtvalue=$(echo "$response" | _normalizeJson | _egrep_o "\"data\".*\][^,]*" | _egrep_o "[^:]*$") + _debug exists_txtvalue "$exists_txtvalue" + else + _err "Error: $response" + return 1 + fi + + if _yc_rest POST "zones/$_domain_id:updateRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":$exists_txtvalue}]}"; then if _contains "$response" "\"done\": true"; then _info "Delete, OK" return 0 @@ -228,7 +194,7 @@ _get_root() { return 1 fi if _contains "$response" "\"zone\": \"$h\""; then - _domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"') + _domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:]*$" | tr -d '"') _debug _domain_id "$_domain_id" if [ "$_domain_id" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") @@ -289,9 +255,7 @@ _yc_login() { _signature=$(printf "%s.%s" "$header" "$payload" | _sign "$YC_SA_Key_File" "sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1" | _url_replace) _debug2 _signature "$_signature" - if [ "$_yc_key_is_temp" ]; then - rm -f "$YC_SA_Key_File" - fi + rm -rf "$YC_SA_Key_File" _jwt=$(printf "{\"jwt\": \"%s.%s.%s\"}" "$header" "$payload" "$_signature") _debug2 _jwt "$_jwt" @@ -300,7 +264,7 @@ _yc_login() { _iam_response="$(_post "$_jwt" "https://iam.api.cloud.yandex.net/iam/v1/tokens" "" "POST")" _debug3 _iam_response "$(echo "$_iam_response" | _normalizeJson)" - YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')" + YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:]*$" | tr -d '"')" _debug3 YC_Token return 0 diff --git a/dnsapi/dns_zonomi.sh b/dnsapi/dns_zonomi.sh index c783a221..ee817381 100644 --- a/dnsapi/dns_zonomi.sh +++ b/dnsapi/dns_zonomi.sh @@ -5,11 +5,9 @@ Site: zonomi.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_zonomi Options: ZM_Key API Key -OptionsAlt: - ZM_Api API endpoint. Default: "https://zonomi.com/app/dns/dyndns.jsp". For RimuHosting use "https://rimuhosting.com/dns/dyndns.jsp". ' -ZM_Api_Default="https://zonomi.com/app/dns/dyndns.jsp" +ZM_Api="https://zonomi.com/app/dns/dyndns.jsp" ######## Public functions ##################### @@ -30,8 +28,6 @@ dns_zonomi_add() { #save the api key to the account conf file. _saveaccountconf_mutable ZM_Key "$ZM_Key" - _zm_init_api - _info "Get existing txt records for $fulldomain" if ! _zm_request "action=QUERY&name=$fulldomain"; then _err "error" @@ -68,27 +64,11 @@ dns_zonomi_rm() { return 1 fi - _zm_init_api - _zm_request "action=DELETE&type=TXT&name=$fulldomain" } #################### Private functions below ################################## - -# resolve the API endpoint: zonomi by default, overridable for providers -# sharing the same API on another host (e.g. RimuHosting) -_zm_init_api() { - ZM_Api="${ZM_Api:-$(_readaccountconf_mutable ZM_Api)}" - if [ -z "$ZM_Api" ]; then - ZM_Api="$ZM_Api_Default" - fi - _debug2 ZM_Api "$ZM_Api" - if [ "$ZM_Api" != "$ZM_Api_Default" ]; then - _saveaccountconf_mutable ZM_Api "$ZM_Api" - fi -} - #qstr _zm_request() { qstr="$1" diff --git a/notify/aws_ses.sh b/notify/aws_ses.sh index 735e6204..07e0c48c 100644 --- a/notify/aws_ses.sh +++ b/notify/aws_ses.sh @@ -83,43 +83,7 @@ aws_ses_send() { response="$(aws_rest POST "" "" "$_data")" } -_use_container_role() { - # automatically set if running inside ECS - if [ -z "$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" ]; then - _debug "No ECS environment variable detected" - return 1 - fi - _use_metadata "169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" -} - -_use_instance_role() { - _instance_role_name_url="http://169.254.169.254/latest/meta-data/iam/security-credentials/" - - if _get "$_instance_role_name_url" true 1 | _head_n 1 | grep -Fq 401; then - _debug "Using IMDSv2" - _token_url="http://169.254.169.254/latest/api/token" - export _H1="X-aws-ec2-metadata-token-ttl-seconds: 21600" - _token="$(_post "" "$_token_url" "" "PUT")" - _secure_debug3 "_token" "$_token" - if [ -z "$_token" ]; then - _debug "Unable to fetch IMDSv2 token from instance metadata" - return 1 - fi - export _H1="X-aws-ec2-metadata-token: $_token" - fi - - if ! _get "$_instance_role_name_url" true 1 | _head_n 1 | grep -Fq 200; then - _debug "Unable to fetch IAM role from instance metadata" - return 1 - fi - - _instance_role_name=$(_get "$_instance_role_name_url" "" 1) - _debug "_instance_role_name" "$_instance_role_name" - _use_metadata "$_instance_role_name_url$_instance_role_name" "$_token" -} - _use_metadata() { - export _H1="X-aws-ec2-metadata-token: $2" _aws_creds="$( _get "$1" "" 1 | _normalizeJson | diff --git a/notify/customscript.sh b/notify/customscript.sh deleted file mode 100644 index ba8b07cb..00000000 --- a/notify/customscript.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env sh - -# Support calling a custom script for notifications -# -# export CUSTOMSCRIPT_PATH="/usr/local/bin/acme-notification.sh" -# -# The script is called with three arguments: -# $1 subject -# $2 content -# $3 status code (0: success, 1: error, 2: skipped) - -customscript_send() { - _subject="$1" - _content="$2" - _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped - _debug "_subject" "$_subject" - _debug "_content" "$_content" - _debug "_statusCode" "$_statusCode" - - CUSTOMSCRIPT_PATH="${CUSTOMSCRIPT_PATH:-$(_readaccountconf_mutable CUSTOMSCRIPT_PATH)}" - if [ -z "$CUSTOMSCRIPT_PATH" ]; then - _err "You didn't specify the custom script path CUSTOMSCRIPT_PATH yet." - return 1 - fi - if ! _exists "$CUSTOMSCRIPT_PATH"; then - _err "The custom script $CUSTOMSCRIPT_PATH does not exist or is not executable." - return 1 - fi - _saveaccountconf_mutable CUSTOMSCRIPT_PATH "$CUSTOMSCRIPT_PATH" - - # Invoke directly, never through eval: the subject and content contain - # domain names and CA messages, eval would allow command injection. - _customscript_result="$("$CUSTOMSCRIPT_PATH" "$_subject" "$_content" "$_statusCode" 2>&1)" - _customscript_rc="$?" - _debug2 "_customscript_result" "$_customscript_result" - - if [ "$_customscript_rc" != "0" ]; then - _err "custom script execution error ($_customscript_rc): $_customscript_result" - return 1 - fi - - _info "custom script executed successfully." - return 0 -} diff --git a/notify/ntfy.sh b/notify/ntfy.sh index 3a788a84..21e39559 100644 --- a/notify/ntfy.sh +++ b/notify/ntfy.sh @@ -14,13 +14,6 @@ ntfy_send() { _debug "_content" "$_content" _debug "_statusCode" "$_statusCode" - _priority_default="default" - _priority_error="high" - - _tag_success="white_check_mark" - _tag_error="warning" - _tag_info="information_source" - NTFY_URL="${NTFY_URL:-$(_readaccountconf_mutable NTFY_URL)}" if [ "$NTFY_URL" ]; then _saveaccountconf_mutable NTFY_URL "$NTFY_URL" @@ -37,26 +30,7 @@ ntfy_send() { export _H1="Authorization: Bearer $NTFY_TOKEN" fi - case "$_statusCode" in - 0) - _priority="$_priority_default" - _tag="$_tag_success" - ;; - 1) - _priority="$_priority_error" - _tag="$_tag_error" - ;; - 2) - _priority="$_priority_default" - _tag="$_tag_info" - ;; - esac - - export _H2="Priority: $_priority" - export _H3="Tags: $_tag" - export _H4="Title: $PROJECT_NAME: $_subject" - - _data="$_content" + _data="${_subject}. $_content" response="$(_post "$_data" "$NTFY_URL/$NTFY_TOPIC" "" "POST" "")" if [ "$?" = "0" ] && _contains "$response" "expires"; then diff --git a/notify/opsgenie.sh b/notify/opsgenie.sh deleted file mode 100644 index d352a18c..00000000 --- a/notify/opsgenie.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env sh - -#Support OpsGenie API integration - -#OPSGENIE_API_KEY="" Required, opsgenie api key -#OPSGENIE_REGION="" Optional, opsgenie region, can be EU or US (default: US) -#OPSGENIE_PRIORITY_SUCCESS="" Optional, opsgenie priority for success (default: P5) -#OPSGENIE_PRIORITY_ERROR="" Optional, opsgenie priority for error (default: P2) -#OPSGENIE_PRIORITY_SKIP="" Optional, opsgenie priority for renew skipped (default: P5) - -_OPSGENIE_AVAIL_REGION="US,EU" -_OPSGENIE_AVAIL_PRIORITIES="P1,P2,P3,P4,P5" - -opsgenie_send() { - _subject="$1" - _content="$2" - _status_code="$3" #0: success, 1: error, 2($RENEW_SKIP): skipped - - OPSGENIE_API_KEY="${OPSGENIE_API_KEY:-$(_readaccountconf_mutable OPSGENIE_API_KEY)}" - if [ -z "$OPSGENIE_API_KEY" ]; then - OPSGENIE_API_KEY="" - _err "You didn't specify an OpsGenie API key OPSGENIE_API_KEY yet." - return 1 - fi - _saveaccountconf_mutable OPSGENIE_API_KEY "$OPSGENIE_API_KEY" - export _H1="Authorization: GenieKey $OPSGENIE_API_KEY" - - OPSGENIE_REGION="${OPSGENIE_REGION:-$(_readaccountconf_mutable OPSGENIE_REGION)}" - if [ -z "$OPSGENIE_REGION" ]; then - OPSGENIE_REGION="US" - _info "The OPSGENIE_REGION is not set, so use the default US as regeion." - elif ! _hasfield "$_OPSGENIE_AVAIL_REGION" "$OPSGENIE_REGION"; then - _err "The OPSGENIE_REGION \"$OPSGENIE_REGION\" is not available, should be one of $_OPSGENIE_AVAIL_REGION" - OPSGENIE_REGION="" - return 1 - else - _saveaccountconf_mutable OPSGENIE_REGION "$OPSGENIE_REGION" - fi - - OPSGENIE_PRIORITY_SUCCESS="${OPSGENIE_PRIORITY_SUCCESS:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_SUCCESS)}" - if [ -z "$OPSGENIE_PRIORITY_SUCCESS" ]; then - OPSGENIE_PRIORITY_SUCCESS="P5" - _info "The OPSGENIE_PRIORITY_SUCCESS is not set, so use the default P5 as priority." - elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_SUCCESS"; then - _err "The OPSGENIE_PRIORITY_SUCCESS \"$OPSGENIE_PRIORITY_SUCCESS\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES" - OPSGENIE_PRIORITY_SUCCESS="" - return 1 - else - _saveaccountconf_mutable OPSGENIE_PRIORITY_SUCCESS "$OPSGENIE_PRIORITY_SUCCESS" - fi - - OPSGENIE_PRIORITY_ERROR="${OPSGENIE_PRIORITY_ERROR:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_ERROR)}" - if [ -z "$OPSGENIE_PRIORITY_ERROR" ]; then - OPSGENIE_PRIORITY_ERROR="P2" - _info "The OPSGENIE_PRIORITY_ERROR is not set, so use the default P2 as priority." - elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_ERROR"; then - _err "The OPSGENIE_PRIORITY_ERROR \"$OPSGENIE_PRIORITY_ERROR\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES" - OPSGENIE_PRIORITY_ERROR="" - return 1 - else - _saveaccountconf_mutable OPSGENIE_PRIORITY_ERROR "$OPSGENIE_PRIORITY_ERROR" - fi - - OPSGENIE_PRIORITY_SKIP="${OPSGENIE_PRIORITY_SKIP:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_SKIP)}" - if [ -z "$OPSGENIE_PRIORITY_SKIP" ]; then - OPSGENIE_PRIORITY_SKIP="P5" - _info "The OPSGENIE_PRIORITY_SKIP is not set, so use the default P5 as priority." - elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_SKIP"; then - _err "The OPSGENIE_PRIORITY_SKIP \"$OPSGENIE_PRIORITY_SKIP\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES" - OPSGENIE_PRIORITY_SKIP="" - return 1 - else - _saveaccountconf_mutable OPSGENIE_PRIORITY_SKIP "$OPSGENIE_PRIORITY_SKIP" - fi - - case "$OPSGENIE_REGION" in - "US") - _opsgenie_url="https://api.opsgenie.com/v2/alerts" - ;; - "EU") - _opsgenie_url="https://api.eu.opsgenie.com/v2/alerts" - ;; - *) - _err "opsgenie region error." - return 1 - ;; - esac - - case $_status_code in - 0) - _priority=$OPSGENIE_PRIORITY_SUCCESS - ;; - 1) - _priority=$OPSGENIE_PRIORITY_ERROR - ;; - 2) - _priority=$OPSGENIE_PRIORITY_SKIP - ;; - *) - _priority=$OPSGENIE_PRIORITY_ERROR - ;; - esac - - _subject_json=$(echo "$_subject" | _json_encode) - _content_json=$(echo "$_content" | _json_encode) - _subject_underscore=$(echo "$_subject" | sed 's/ /_/g') - _alias_json=$(echo "acme.sh-$(hostname)-$_subject_underscore-$(date +%Y%m%d)" | base64 --wrap=0 | _json_encode) - - _data="{ - \"message\": \"$_subject_json\", - \"alias\": \"$_alias_json\", - \"description\": \"$_content_json\", - \"tags\": [ - \"acme.sh\", - \"host:$(hostname)\" - ], - \"entity\": \"$(hostname -f)\", - \"priority\": \"$_priority\" -}" - - if response=$(_post "$_data" "$_opsgenie_url" "" "" "application/json"); then - if ! _contains "$response" error; then - _info "opsgenie send success." - return 0 - fi - fi - _err "opsgenie send error." - _err "$response" - return 1 -} diff --git a/notify/pushover.sh b/notify/pushover.sh index c59ec026..0f99739a 100644 --- a/notify/pushover.sh +++ b/notify/pushover.sh @@ -46,8 +46,8 @@ pushover_send() { fi export _H1="Content-Type: application/json" - _content="$(printf "%s" "$_content" | _json_encode)" - _subject="$(printf "%s" "$_subject" | _json_encode)" + _content="$(printf "*%s*\n" "$_content" | _json_encode)" + _subject="$(printf "*%s*\n" "$_subject" | _json_encode)" _data="{\"token\": \"$PUSHOVER_TOKEN\",\"user\": \"$PUSHOVER_USER\",\"title\": \"$_subject\",\"message\": \"$_content\",\"sound\": \"$PUSHOVER_SOUND\", \"device\": \"$PUSHOVER_DEVICE\", \"priority\": \"$PUSHOVER_PRIORITY\"}" response="$(_post "$_data" "$PUSHOVER_URI")" diff --git a/notify/smtp.sh b/notify/smtp.sh index a7318692..f5ebebca 100644 --- a/notify/smtp.sh +++ b/notify/smtp.sh @@ -200,7 +200,6 @@ _smtp_send_curl() { set -- "$@" \ --upload-file - \ - --crlf \ --mail-from "$SMTP_FROM" \ --max-time "$SMTP_TIMEOUT" diff --git a/notify/telegram.sh b/notify/telegram.sh old mode 100755 new mode 100644 index 1f379f19..ccbd1533 --- a/notify/telegram.sh +++ b/notify/telegram.sh @@ -5,10 +5,6 @@ #TELEGRAM_BOT_APITOKEN="" #TELEGRAM_BOT_CHATID="" #TELEGRAM_BOT_URLBASE="" -#TELEGRAM_BOT_THREADID="" - -# To get TELEGRAM_BOT_THREADID, just copy the link of the message from the thread. -# https://t.me/c/123456789/XXX/1520 - XXX is the TELEGRAM_BOT_THREADID telegram_send() { _subject="$1" @@ -32,26 +28,17 @@ telegram_send() { fi _saveaccountconf_mutable TELEGRAM_BOT_CHATID "$TELEGRAM_BOT_CHATID" - TELEGRAM_BOT_THREADID="${TELEGRAM_BOT_THREADID:-$(_readaccountconf_mutable TELEGRAM_BOT_THREADID)}" - if [ -z "$TELEGRAM_BOT_THREADID" ]; then - TELEGRAM_BOT_THREADID="" - fi - _saveaccountconf_mutable TELEGRAM_BOT_THREADID "$TELEGRAM_BOT_THREADID" - TELEGRAM_BOT_URLBASE="${TELEGRAM_BOT_URLBASE:-$(_readaccountconf_mutable TELEGRAM_BOT_URLBASE)}" if [ -z "$TELEGRAM_BOT_URLBASE" ]; then TELEGRAM_BOT_URLBASE="https://api.telegram.org" fi _saveaccountconf_mutable TELEGRAM_BOT_URLBASE "$TELEGRAM_BOT_URLBASE" - _subject="$(printf "%s" "$_subject" | sed -E 's/([][()~`>#+=|{}.!*_\\-])/\\\\\1/g')" - _content="$(printf "%s" "$_content" | sed -E 's/([][()~`>#+=|{}.!*_\\-])/\\\\\1/g')" + _subject="$(printf "%s" "$_subject" | sed 's/\\/\\\\\\\\/g' | sed 's/\]/\\\\\]/g' | sed 's/\([_*[()~`>#+--=|{}.!]\)/\\\\\1/g')" + _content="$(printf "%s" "$_content" | sed 's/\\/\\\\\\\\/g' | sed 's/\]/\\\\\]/g' | sed 's/\([_*[()~`>#+--=|{}.!]\)/\\\\\1/g')" _content="$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode)" _data="{\"text\": \"$_content\", " _data="$_data\"chat_id\": \"$TELEGRAM_BOT_CHATID\", " - if [ -n "$TELEGRAM_BOT_THREADID" ]; then - _data="$_data\"message_thread_id\": \"$TELEGRAM_BOT_THREADID\", " - fi _data="$_data\"parse_mode\": \"MarkdownV2\", " _data="$_data\"disable_web_page_preview\": \"1\"}" diff --git a/notify/waha.sh b/notify/waha.sh deleted file mode 100755 index 573295e9..00000000 --- a/notify/waha.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env sh - -#Support WAHA (WhatsApp HTTP API) - free, self-hosted WhatsApp API -#https://waha.devlike.pro/ - -#Required: -#WAHA_URL="http://localhost:3000" -#WAHA_CHAT_ID="1234567890@c.us" - -#Optional: -#WAHA_API_KEY="" -#WAHA_SESSION="default" - -waha_send() { - _subject="$1" - _content="$2" - _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped - _debug "_subject" "$_subject" - _debug "_content" "$_content" - _debug "_statusCode" "$_statusCode" - - WAHA_URL="${WAHA_URL:-$(_readaccountconf_mutable WAHA_URL)}" - if [ -z "$WAHA_URL" ]; then - WAHA_URL="" - _err "You didn't specify the WAHA server url WAHA_URL yet." - _err "Example: export WAHA_URL=\"http://localhost:3000\"" - return 1 - fi - _saveaccountconf_mutable WAHA_URL "$WAHA_URL" - - WAHA_CHAT_ID="${WAHA_CHAT_ID:-$(_readaccountconf_mutable WAHA_CHAT_ID)}" - if [ -z "$WAHA_CHAT_ID" ]; then - WAHA_CHAT_ID="" - _err "You didn't specify the WhatsApp chat id WAHA_CHAT_ID yet." - _err "Example: export WAHA_CHAT_ID=\"1234567890@c.us\"" - return 1 - fi - _saveaccountconf_mutable WAHA_CHAT_ID "$WAHA_CHAT_ID" - - WAHA_API_KEY="${WAHA_API_KEY:-$(_readaccountconf_mutable WAHA_API_KEY)}" - if [ "$WAHA_API_KEY" ]; then - _saveaccountconf_mutable WAHA_API_KEY "$WAHA_API_KEY" - fi - - WAHA_SESSION="${WAHA_SESSION:-$(_readaccountconf_mutable WAHA_SESSION)}" - if [ -z "$WAHA_SESSION" ]; then - WAHA_SESSION="default" - else - _saveaccountconf_mutable WAHA_SESSION "$WAHA_SESSION" - fi - - _content=$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode) - - _data="{\"chatId\": \"$WAHA_CHAT_ID\", " - _data="$_data\"text\": \"$_content\", " - _data="$_data\"session\": \"$WAHA_SESSION\"}" - - _debug "_data" "$_data" - - if [ "$WAHA_API_KEY" ]; then - export _H1="X-Api-Key: $WAHA_API_KEY" - fi - - _waha_url="${WAHA_URL}/api/sendText" - response="$(_post "$_data" "$_waha_url" "" "POST" "application/json")" - - if [ "$?" = "0" ] && _contains "$response" "\"id\""; then - _info "waha send success." - return 0 - fi - _err "waha send error." - _err "$response" - return 1 -}