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..00d180b9 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@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file @@ -114,11 +114,9 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - 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@v6 - 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,72 +224,12 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/freebsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} - 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" - - - - 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 @@ -329,7 +262,7 @@ jobs: OpenBSD: runs-on: ubuntu-latest - needs: GhostBSD + needs: FreeBSD env: TEST_DNS : ${{ secrets.TEST_DNS }} TestingDomain: ${{ secrets.TestingDomain }} @@ -347,13 +280,12 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} - 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 @@ -404,13 +336,12 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/netbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} - 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 @@ -462,13 +393,12 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/dragonflybsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} - 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 @@ -524,13 +454,12 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/midnightbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} - 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 @@ -582,13 +511,12 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/solaris-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} - 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: | @@ -642,13 +570,12 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/omnios-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} - 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 @@ -699,13 +626,12 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openindiana-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} - 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 @@ -735,66 +661,9 @@ jobs: - 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 + needs: OpenIndiana env: TEST_DNS : ${{ secrets.TEST_DNS }} TestingDomain: ${{ secrets.TestingDomain }} @@ -813,140 +682,19 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/haiku-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} - 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}}" diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index 20c61dcc..c8cbc985 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -58,7 +58,6 @@ jobs: - 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" diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index 88ef0a6e..50fcab32 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -64,7 +64,6 @@ jobs: - 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" 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 index b133dd18..9884ebeb 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -65,7 +65,6 @@ jobs: - 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" 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/MidnightBSD.yml b/.github/workflows/MidnightBSD.yml index ce499e4e..15024833 100644 --- a/.github/workflows/MidnightBSD.yml +++ b/.github/workflows/MidnightBSD.yml @@ -58,7 +58,6 @@ jobs: - 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" diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index 6695f71e..16d0ae2d 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -58,7 +58,6 @@ jobs: - 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" 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..eb486b35 100644 --- a/.github/workflows/Omnios.yml +++ b/.github/workflows/Omnios.yml @@ -64,7 +64,6 @@ jobs: - 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" diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index 46318163..4fdb76c5 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -64,7 +64,6 @@ jobs: - 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" 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 index e3119f8e..b5061ba7 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -64,7 +64,6 @@ jobs: - 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" diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 30e4e291..3393269a 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -64,7 +64,6 @@ jobs: - 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" 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/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..0e7ba748 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 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 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..558ebf48 100644 --- a/.github/workflows/pr_dns.yml +++ b/.github/workflows/pr_dns.yml @@ -13,7 +13,7 @@ jobs: 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/pr_notify.yml b/.github/workflows/pr_notify.yml index 76ae76f6..416ed721 100644 --- a/.github/workflows/pr_notify.yml +++ b/.github/workflows/pr_notify.yml @@ -15,7 +15,7 @@ jobs: 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/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 index 89bb1f3a..a706529a 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -9,14 +9,13 @@ jobs: if: github.actor != 'neilpang' steps: - name: Checkout wiki repository - uses: actions/checkout@v7 + uses: actions/checkout@v6 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") @@ -28,17 +27,6 @@ jobs: 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 @@ -53,21 +41,17 @@ jobs: 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 "[Click here to Revert](${page_url}/_history)" echo "" echo "----" - echo "### diff:" + echo "### diff:" echo '```diff' cat wiki.diff echo '```' } > wiki-change-msg.txt - echo "notify=true" >> "$GITHUB_OUTPUT" - name: Create issue to notify Neilpang - if: steps.msg.outputs.notify == 'true' - uses: peter-evans/create-issue-from-file@v6 + uses: peter-evans/create-issue-from-file@v5 with: title: "Wiki edited" content-filepath: ./wiki-change-msg.txt 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..55a9cc67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -81,8 +81,8 @@ if [ \"\$1\" = \"daemon\" ]; 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 \ + random_hour=\$((\$time / 60 % 24)) \n \ + echo \"\$random_minute \$random_hour * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \ fi \n \ echo \"Running Supercronic using crontab at \$LE_CONFIG_HOME/crontab\" \n \ exec -- /usr/bin/supercronic \"\$LE_CONFIG_HOME/crontab\" \n \ diff --git a/README.md b/README.md index 90280e94..188af0e1 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,7 @@

- - - - - - - - ZeroSSL - - + + zerossl.com +

🔐 acme.sh

@@ -31,13 +17,9 @@ Solaris DragonFlyBSD MidnightBSD - GhostBSD Omnios OpenIndiana - Tribblix Haiku - Hurd - OpenEuler

@@ -130,10 +112,6 @@ |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) @@ -168,7 +146,6 @@ | 🌐 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/)) | --- @@ -419,53 +396,7 @@ acme.sh --renew -d example.com --- -### 🔟 Use DNS Persist Mode - -📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode - -📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/) - -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 - -```bash -acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] [--dns-persist-days 365] -``` - -Options: - -| Flag | Description | -|------|-------------| -| `--server ` | 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. | - -You should get an output like: - -```sh -TXT persist domain:_validation-persist.example.com -TXT persist value :"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789" -``` - -#### ✍️ Step 2: Add the TXT record to your DNS - -Add the printed `TXT persist domain` / `TXT persist value` pair as a TXT record at your DNS provider, then wait for it to propagate. - -#### 📜 Step 3: Issue the certificate - -```bash -acme.sh --issue -d example.com --dns-persist -``` - -✅ **Done!** No challenge token is provisioned during issuance — the CA reads the persistent TXT record directly. - -> 🔄 Renewals just work: `acme.sh --renew -d example.com` (or the cron job) reuses the same TXT record automatically — no further DNS edits needed. - ---- - -### 1️⃣1️⃣ Issue Certificates of Different Key Types (ECC or RSA) +### 🔟 Issue Certificates of Different Key Types (ECC or RSA) Just set the `keylength` to a valid, supported value. @@ -496,7 +427,7 @@ acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com - --- -### 1️⃣2️⃣ Issue Wildcard Certificates +### 1️⃣1️⃣ Issue Wildcard Certificates It's simple! Just give a wildcard domain as the `-d` parameter: @@ -508,9 +439,9 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf --- -### 1️⃣3️⃣ How to Renew Certificates +### 1️⃣2️⃣ How to Renew Certificates -> 🔄 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). +> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days. However, you can force a renewal: @@ -524,43 +455,9 @@ acme.sh --renew -d example.com --force 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 +### 1️⃣3️⃣ How to Stop Certificate Renewal To stop renewal of a cert, you can execute the following to remove the cert from the renewal list: @@ -574,7 +471,7 @@ The cert/key file is not removed from the disk. --- -### 1️⃣5️⃣ How to Upgrade acme.sh +### 1️⃣4️⃣ How to Upgrade acme.sh > 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code. @@ -598,25 +495,25 @@ acme.sh --upgrade --auto-upgrade 0 --- -### 1️⃣6️⃣ Issue a Certificate from an Existing CSR +### 1️⃣5️⃣ Issue a Certificate from an Existing CSR 📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR --- -### 1️⃣7️⃣ Send Notifications in Cronjob +### 1️⃣6️⃣ Send Notifications in Cronjob 📚 https://github.com/acmesh-official/acme.sh/wiki/notify --- -### 1️⃣8️⃣ Under the Hood +### 1️⃣7️⃣ Under the Hood > 🔧 Speak ACME language using shell, directly to "Let's Encrypt". --- -### 1️⃣9️⃣ Acknowledgments +### 1️⃣8️⃣ Acknowledgments | Project | Link | |---------|------| @@ -633,8 +530,6 @@ 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 Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)] @@ -660,7 +555,7 @@ Support this project with your organization. Your logo will show up here with a --- -### 2️⃣0️⃣ License & Others +### 1️⃣9️⃣ License & Others 📄 **License:** GPLv3 @@ -670,7 +565,7 @@ Support this project with your organization. Your logo will show up here with a --- -### 2️⃣1️⃣ Donate +### 2️⃣0️⃣ Donate > 💝 Your donation makes **acme.sh** better! @@ -682,7 +577,7 @@ Support this project with your organization. Your logo will show up here with a --- -### 2️⃣2️⃣ About This Repository +### 2️⃣1️⃣ 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! diff --git a/acme.sh b/acme.sh index 4d4f6cbe..e780f61d 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.1.5 +VER=3.1.3 PROJECT_NAME="acme.sh" @@ -59,7 +59,6 @@ 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" @@ -72,7 +71,6 @@ NO_VALUE="no" W_DNS="dns" W_ALPN="alpn" -W_DNS_PERSIST="dns_persist" DNS_ALIAS_PREFIX="=" MODE_STATELESS="stateless" @@ -918,15 +916,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 +1013,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() { @@ -1066,25 +1037,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 +1140,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 +1162,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 +1175,6 @@ _createkey() { return 1 fi chmod 600 "$f" - _new_key_file="1" fi if _isEccKey "$length"; then @@ -1238,10 +1183,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 +1195,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 @@ -1307,23 +1244,6 @@ _idn() { } #_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() { _debug _createcsr domain="$1" @@ -1387,16 +1307,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 +1342,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 +1357,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 +1383,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" @@ -1547,22 +1464,6 @@ _toPkcs() { } -_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" - fi - -} - #domain [password] [isEcc] toPkcs() { domain="$1" @@ -1584,21 +1485,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" @@ -1932,89 +1832,22 @@ _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 + #Omnios + 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,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-%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 @@ -2641,13 +2474,6 @@ _savedeployconf() { _cleardomainconf "$1" } -#key -_cleardeployconf() { - _cleardomainconf "SAVED_$1" - #remove later - _cleardomainconf "$1" -} - #key _getdeployconf() { _rac_key="$1" @@ -2715,21 +2541,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,24 +2554,16 @@ _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 + if [ "$Le_Listen_V6" ]; then _NC="$_NC -6" SOCAT_OPTIONS=TCP6-LISTEN - elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then + elif [ "$Le_Listen_V4" ]; 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 + SOCAT_OPTIONS=TCP-LISTEN fi if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then @@ -2768,10 +2571,6 @@ _startserver() { 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 @@ -2780,14 +2579,14 @@ _startserver() { _content_len="$(printf "%s" "$content" | wc -c)" _debug _content_len "$_content_len" + _debug "_NC" "$_NC $SOCAT_OPTIONS" 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_OPTIONS SYSTEM:"sleep 1; \ +echo 'HTTP/1.0 200 OK'; \ +echo 'Content-Length\: $_content_len'; \ +echo ''; \ +printf '%s' '$content';" 2>"$_SOCAT_ERR" & + serverproc="$!" else _PYTHON="" if _exists "python3"; then @@ -2799,40 +2598,21 @@ _startserver() { 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 + _AF="socket.AF_INET" + _BIND_ADDR="0.0.0.0" + if [ "$Le_Listen_V6" ]; then + _AF="socket.AF_INET6" _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) + $_PYTHON -c "import socket,sys;s=socket.socket($_AF,socket.SOCK_STREAM);s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind((sys.argv[2],int(sys.argv[1])));s.listen(5);res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3]; while True: - 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" & + c,a=s.accept() + c.sendall(res.encode() if hasattr(res, 'encode') else res) + c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" & serverproc="$!" _NC="$_PYTHON" else @@ -2855,11 +2635,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 +2708,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 +2776,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 @@ -3137,11 +2879,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 +3309,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 +3455,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 @@ -4027,10 +3759,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 +3773,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 +3792,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 +3809,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 +3823,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 +3832,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 +3851,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 +3870,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 +3878,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 +3914,7 @@ _regAccount() { fi _savecaconf "ACCOUNT_URL" "$_accUri" else - _accUri="$(_readcaconf ACCOUNT_URL)" + ACCOUNT_URL="$(_readcaconf ACCOUNT_URL)" fi export ACCOUNT_URL="$_accUri" @@ -4206,9 +3922,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 +3956,7 @@ updateaccount() { _email="$(_getAccountEmail)" if [ "$_email" ]; then - updjson='{"contact": ['$(echo "$_email" | _mailto_contacts)']}' + updjson='{"contact": ["mailto:'$_email'"]}' else updjson='{"contact": []}' fi @@ -4252,12 +3966,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 +3975,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 +4002,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 +4028,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 +4377,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 } @@ -5002,18 +4485,11 @@ issue() { 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,7 +4499,7 @@ 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 + 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" @@ -5075,13 +4551,6 @@ 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 @@ -5111,7 +4580,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 @@ -5240,45 +4709,13 @@ issue() { 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 +4740,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 +4806,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 @@ -5414,7 +4849,7 @@ $_authorizations_map" 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" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$keyauthorization" -a -z "$entry" ]; then @@ -5429,7 +4864,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 +4884,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 +4921,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 +5115,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 +5170,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" @@ -5921,14 +5345,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 +5364,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 +5427,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 +5451,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,75 +5472,25 @@ $_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" @@ -6143,12 +5500,6 @@ $_authorizations_map" _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 - if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then _savedomainconf "Le_RealCertPath" "$_real_cert" _savedomainconf "Le_RealCACertPath" "$_real_ca" @@ -6167,18 +5518,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 +5546,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 @@ -6239,14 +5578,6 @@ renew() { 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 +5586,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 @@ -6368,8 +5629,11 @@ renew() { 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" 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,10 +5673,6 @@ 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 _debug di "$di" if ! [ -d "$di" ]; then @@ -6421,19 +5681,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 +5701,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 @@ -6697,7 +5945,7 @@ list_profiles() { fi normalized_response=$(echo "$response" | _normalizeJson) - profiles_json=$(echo "$normalized_response" | _egrep_o '"profiles" *: *[{][^}]*[}]') + 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." @@ -6759,45 +6007,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 +6027,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 +6160,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 +6182,6 @@ _install_win_taskscheduler() { _lesh="$1" _centry="$2" _randomminute="$3" - _randomhour="$4" if ! _exists cygpath; then _err "cygpath not found" return 1 @@ -7051,10 +6208,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 +6251,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 +6260,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 +6274,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 +6478,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." @@ -7380,7 +6511,7 @@ _deactivate() { _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" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$entry" ]; then _err "$d: Cannot get domain token" @@ -7464,60 +6595,34 @@ 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 + openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ' :' } #cert _getSerial() { _cert="$1" - ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -serial -noout | cut -d = -f 2 + 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() { +_get_ARI() { _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)" + _akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)" _debug2 "_akiurl" "$_akiurl" - _serurl="$(echo "$_ser" | _h2b | _base64 | _url_replace)" + _serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)" _debug2 "_serurl" "$_serurl" - printf "%s.%s" "$_akiurl" "$_serurl" -} - -#cert -_get_ARI() { - _cert="$1" - _ari_certID="$(_getARICertID "$_cert")" - _ARI_URL="$ACME_RENEWAL_INFO/$_ari_certID" + _ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl" _get "$_ARI_URL" + } # Detect profile file if not specified as environment variable @@ -7605,9 +6710,9 @@ _precheck() { 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." + _err "It is recommended to install socat or python first." + _err "We use socat or python for the standalone server, which is used for standalone mode." + _err "If you don't want to use standalone mode, you may ignore this warning." fi return 0 @@ -7631,15 +6736,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 +6753,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 +6776,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 +6788,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 +6862,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 +6892,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 +6902,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 +6927,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 +6941,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,24 +6977,20 @@ 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 @@ -7977,14 +7039,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 +7156,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. @@ -8154,34 +7205,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,11 +7233,8 @@ 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. @@ -8230,9 +7260,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. + --listen-v4 Force standalone/tls server to listen at ipv4. + --listen-v6 Force standalone/tls server to listen at ipv6. --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. --openssl-bin Specifies a custom openssl bin location. @@ -8254,17 +7283,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 +7313,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 +7321,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 +7342,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 @@ -8397,16 +7413,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 @@ -8576,9 +7585,6 @@ _process() { _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 +7673,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" ;; @@ -8833,14 +7822,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" @@ -9207,22 +8188,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 @@ -9270,15 +8235,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 +8251,7 @@ _process() { toPkcs "$_domain" "$_password" "$_ecc" ;; toPkcs8) - toPkcs8 "$_domain" "$_password" "$_ecc" + toPkcs8 "$_domain" "$_ecc" ;; createAccountKey) createAccountKey "$_accountkeylength" 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/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 index 394b431f..8443bb99 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -163,8 +163,8 @@ byteplus_alb_deploy() { # ── 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') + _public_key=$(sed '/^[[:space:]]*$/d' "$_cfullchain" | tr -d '\r') + _private_key=$(sed '/^[[:space:]]*$/d' "$_ckey" | tr -d '\r') if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then _err "Failed to read certificate or key file." 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/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/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/keyhelp.sh b/deploy/keyhelp.sh index f66d27ce..97f9c21c 100644 --- a/deploy/keyhelp.sh +++ b/deploy/keyhelp.sh @@ -83,7 +83,7 @@ keyhelp_deploy() { _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/ *$//') + _message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/
/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//') _info "_message" "$_message" if [ -z "$_message" ]; then _err "Fail to upload certificate." @@ -118,7 +118,7 @@ keyhelp_deploy() { _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/ *$//') + _message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/
/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//') _info "_message" "$_message" if [ -z "$_message" ]; then _err "Fail to apply certificate." diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 4a8c9dc9..ef920f64 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -10,10 +10,6 @@ # 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 @@ -53,7 +49,7 @@ multideploy_deploy() { _debug _cfullchain "$_cfullchain" _debug _cpfx "$_cpfx" - _getdeployconf MULTIDEPLOY_FILENAME + MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}" if [ -z "$MULTIDEPLOY_FILENAME" ]; then MULTIDEPLOY_FILENAME="multideploy.yml" _info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'." @@ -79,8 +75,7 @@ multideploy_deploy() { # 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. +# $@ - Posible deploy file names. # Usage: # _preprocess_deployfile "" "?" _preprocess_deployfile() { @@ -92,21 +87,15 @@ _preprocess_deployfile() { _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 "Checking file" "$DOMAIN_PATH/$file" + if [ -f "$DOMAIN_PATH/$file" ]; then _debug3 "File found" if [ -n "$found_file" ]; then _err "Multiple deploy files found. Please keep only one deploy file." return 1 fi - found_file="$_multideploy_path" + found_file="$file" else _debug3 "File not found" fi @@ -116,12 +105,12 @@ _preprocess_deployfile() { _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" + if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then + _err "Deploy file is not valid: $DOMAIN_PATH/$found_file" return 1 fi - echo "$found_file" + echo "$DOMAIN_PATH/$found_file" } # Description: @@ -221,7 +210,7 @@ _clear_envs() { echo "$env_pairs" | while IFS='=' read -r _key _value; do _debug3 "Deleting key" "$_key" - _cleardeployconf "$_key" + _cleardomainconf "SAVED_$_key" unset -v "$_key" done } 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/panos.sh b/deploy/panos.sh index fcfd6fb5..00badffc 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -296,20 +296,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 + deployer cert + deployer key + deployer commit if [ "$_panos_template_stack" ]; then # try to get job status for 20 times in 30 sec interval i=0 diff --git a/deploy/proxmoxbs.sh b/deploy/proxmoxbs.sh index 30599a44..179b0369 100644 --- a/deploy/proxmoxbs.sh +++ b/deploy/proxmoxbs.sh @@ -116,24 +116,17 @@ HEREDOC 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 + _retval=$? + if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then + _debug3 response "$response" + _info "Certificate successfully deployed" + return 0 + else + _err "Certificate deployment failed: $message" + _debug "Response" "$response" + return 1 + fi } diff --git a/deploy/proxmoxve.sh b/deploy/proxmoxve.sh index fd8d69d8..b6298ee7 100644 --- a/deploy/proxmoxve.sh +++ b/deploy/proxmoxve.sh @@ -128,24 +128,17 @@ HEREDOC 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 + _retval=$? + if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then + _debug3 response "$response" + _info "Certificate successfully deployed" + return 0 + else + _err "Certificate deployment failed: $message" + _debug "Response" "$response" + return 1 + fi } 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..848380a5 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() { @@ -453,13 +428,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/synology_dsm.sh b/deploy/synology_dsm.sh index 336980a5..e28a4036 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 @@ -355,7 +354,6 @@ synology_dsm_deploy() { if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then _err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set." - _logout _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" return 1 fi @@ -391,13 +389,13 @@ synology_dsm_deploy() { else _info "Restart HTTP services not necessary." 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..df34f927 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)" - 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 'cookie1984nammnamm=[^;]*;' | 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 'cookie1984nammnamm=[^;]*;' | 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_ali.sh b/dnsapi/dns_ali.sh index b8ca9169..90196c69 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,20 +96,6 @@ _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 diff --git a/dnsapi/dns_arubabusiness.sh b/dnsapi/dns_arubabusiness.sh deleted file mode 100644 index 90b3f18d..00000000 --- a/dnsapi/dns_arubabusiness.sh +++ /dev/null @@ -1,490 +0,0 @@ -#!/usr/bin/env sh - -# shellcheck disable=SC2034 -dns_arubabusiness_info='ArubaBusiness -Site: business.aruba.it -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_arubabusiness -Options: - AB_Key Your ArubaBusiness API Key - AB_User Your account user - AB_Pass Your account password -' - -# -# A word of warning: as of this writing, api.arubabusiness.it only supports oauth authentication using the "password" grant type. -# If you are REALLY sure you want to use it, it would be wise set up a dedicated technical user without administrative privileges -# - -ARUBABUSINESS_API='https://api.arubabusiness.it' - -######## Public functions ######## - -# -# Usage: dns_arubabusiness_add _acme-challenge.www.domain.com aaaabbbbcccc111122223333 -# -# Add a new TXT record whose name and value match the given domain and value -# -# Variables -# _full_domain: $1 - the name of the TXT record -# _txt_value: $2 - the value of the TXT record -# _body -# dns_details -# domain_id -# dns_record_id -# response -# -dns_arubabusiness_add() { - _full_domain=$1 - _txt_value=$2 - - if ! _ab_authenticate; then - return 1 - fi - - if ! _ab_domain_id "$_full_domain"; then - return 1 - fi - - if _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then - # This is very unlikely, but allow the process to use the existing record - _info "A TXT record with name: $_full_domain and value: $_txt_value already exists (id: $dns_record_id)" - return 0 - fi - - _body="{ \"IdDomain\": $domain_id, \"Type\": \"TXT\", \"Name\": \"$_full_domain\", \"Content\": \"\\\"$_txt_value\\\"\" }" - - _debug "Adding TXT record with name: $_full_domain and value: $_txt_value" - - if ! _ab_rest POST "api/domains/dns/record" "$_body" || ! _contains "$response" "DomainId"; then - _err "Failed to add TXT record with name: $_full_domain" - return 1 - fi - - _info "Sleeping 10 seconds to let ArubaBusiness do its magic" - _sleep 10 - - # Refresh dns details and check that the record was really added - if ! _ab_dns_details "$root_domain"; then - return 1 - fi - - if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then - # This should never happen - _err "The TXT record with name: $_full_domain was not set" - _err "Please check that the dns records are clean" - return 1 - fi - - _info "Added TXT record with id: $dns_record_id" - return 0 -} - -# -# Usage: dns_arubabusiness_rm _acme-challenge.www.domain.com aaaabbbbcccc111122223333 -# -# Remove the TXT record whose name and value match the given domain and value -# -# Variables -# _full_domain: $1 - the name of the TXT record -# _txt_value: $2 - the value of the TXT record -# dns_details -# dns_record_id -# -dns_arubabusiness_rm() { - _full_domain=$1 - _txt_value=$2 - - if ! _ab_authenticate; then - return 1 - fi - - if ! _ab_domain_id "$_full_domain"; then - return 1 - fi - - if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details" || [ -z "$dns_record_id" ]; then - _err "Could not retrieve the record id for: $_full_domain" - return 1 - fi - - _debug "Deleting TXT record: $dns_record_id" - if ! _ab_rest DELETE "api/domains/dns/record/$dns_record_id" || ! _contains "$response" "DomainId"; then - _err "Failed to delete TXT record: $dns_record_id" - return 1 - fi - - _info "Deleted TXT record: $dns_record_id" - return 0 -} - -######## Private functions ######## - -# -# Usage: _ab_domain_id _acme-challenge.www.domain.com -# -# Split the input domain into subdomain + root domain and get the id of the root domain -# -# Variables -# _full_domain: $1 - the domain whose root needs to be extracted -# _domain_sections -# _current_index -# _candidate_subdomain -# _candidate_domain -# sub_domain -# root_domain -# domain_id -# dns_details: a json containing all dns records registered on the root domain -# -# Example -# _get_root _acme-challenge.www.domain.com -# -# Should return -# sub_domain=_acme-challenge.www -# root_domain=domain.com -# domain_id=123123123123 -# dns_details="{JSON_CONTENT}" -# -_ab_domain_id() { - _full_domain=$1 - - _info "Attempting to retrieve root domain details for: $_full_domain" - - _domain_sections=$(_math "$(printf "%s" "$_full_domain" | tr '.' '\n' | wc -l)" + 1) - - if [ "$_domain_sections" -lt 1 ]; then - _err "Invalid input $_full_domain" - return 1 - fi - - _current_index=1 - while true; do - _candidate_subdomain=$(if [ "$_current_index" = "1" ]; then printf ""; else printf "%s" "$_full_domain" | cut -d . -f 1-"$(_math "$_current_index" - 1)"; fi) - _candidate_domain=$(printf "%s" "$_full_domain" | cut -d . -f "$_current_index"-"$_domain_sections") - - if ! _ab_dns_details "$_candidate_domain"; then - _debug2 "Could not fetch dns details for: $_candidate_domain" - _current_index=$(_math "$_current_index" + 1) - - # Fail if there are no candidates left - if [ "$_current_index" -gt "$_domain_sections" ]; then - _err "Could not determine the root domain for: $_full_domain" - return 1 - fi - else - sub_domain="$_candidate_subdomain" - root_domain="$_candidate_domain" - # Extract the domain id, which is an integer and contains no commas - domain_id="$(printf "%s" "$dns_details" | _egrep_o '"Id":[^,]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')" - - if [ -z "$domain_id" ]; then - _err "Could not determine the domain id for: $root_domain" - return 1 - fi - - _debug "Retrieved root domain id: $domain_id" - return 0 - fi - done -} - -# -# Usage: _ab_dns_record_id _acme-challenge.www.domain.com "aaaabbbbcccc111122223333" "{JSON_CONTENT}" -# -# Extract the record id of the first TXT record whose name and content match the input values -# -# Variables -# _record_name: $1 -# _txt_value: $2 -# _dns_details: $3 - the json returned by a previous call to '_ab_dns_details() $root_domain' -# _record_ids -# _record_names -# _record_types -# _record_contents -# _record_ids_count -# _record_names_count -# _record_types_count -# _record_contents_count -# _i -# dns_record_id -# -# Notes -# TXT correspond to record type 5 -# ArubaBusiness appends a terminating dot (.) to the record name -# The content field may contain the following character sequence: \" -# All record names are always converted to lowercase -# -_ab_dns_record_id() { - _record_name=$1 - _txt_value=$2 - _dns_details=$3 - - _record_name_lowercase=$(printf "%s" "$_record_name" | _lower_case) - - # Extract the record ids, which are integers and contain no commas, colons or spaces - # The first id is skipped because it refers to the domain id - _record_ids=$(printf "%s" "$_dns_details" | sed 's/"Id":/\n"Id":/g' | _egrep_o '"Id":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ') - - # Extract the record names, which are strings but cannot contain commas, colons, spaces and quotes - # The first name is skipped because it refers to the domain name - _record_names=$(printf "%s" "$_dns_details" | sed 's/"Name":/\n"Name":/g' | _egrep_o '"Name":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' "' | tr '\n' ' ') - - # Extract the record types, which are integers (except for the first one) and contain no commas, colons or spaces - # The first type is skipped because it refers to the domain type - _record_types=$(printf "%s" "$_dns_details" | sed 's/"Type":/\n"Type":/g' | _egrep_o '"Type":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ') - - # Extract the record contents, which are strings and may contain no quotes except for TXT records, which must be delimited by two \" literals - # Note: There is no domain related entry here - # Note: A " character is appended at the end of each content to make it easier to process the list later - _record_contents=$(printf "%s" "$_dns_details" | sed 's/"Content":/\n"Content":/g' | sed 's/\\"//g' | _egrep_o '"Content": *"[^"]*"' | cut -d : -f 2- | sed -n 's/"\(.*\)"/\1/p' | tr '\n' '#') - - _info "IDS: $_record_ids" - _info "NAMES: $_record_names" - _info "TYPEs: $_record_types" - _info "CONTENTS: $_record_contents" - - _record_ids_count=$(printf "%s" "$_record_ids" | tr ' ' '\n' | wc -l) - _record_names_count=$(printf "%s" "$_record_names" | tr ' ' '\n' | wc -l) - _record_types_count=$(printf "%s" "$_record_types" | tr ' ' '\n' | wc -l) - _record_contents_count=$(printf "%s" "$_record_contents" | tr '#' '\n' | wc -l) - - _info "Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count" - - if [ "$_record_ids_count" != "$_record_names_count" ] || [ "$_record_ids_count" != "$_record_types_count" ] || [ "$_record_ids_count" != "$_record_contents_count" ]; then - _err "Failed to parse record elements. Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count" - return 1 - fi - - _info "Looking for a TXT record matching inputs - name: $_record_name_lowercase value: $_txt_value" - - _i=1 - while [ "$_i" -le "$_record_ids_count" ]; do - _current_name=$(printf "%s" "$_record_names" | cut -d " " -f "$_i") - _current_type=$(printf "%s" "$_record_types" | cut -d " " -f "$_i") - _current_content=$(printf "%s" "$_record_contents" | cut -d "#" -f "$_i") - - if [ "$_record_name_lowercase." = "$_current_name" ] && [ "5" = "$_current_type" ] && [ "$_txt_value" = "$_current_content" ]; then - dns_record_id=$(printf "%s" "$_record_ids" | cut -d " " -f "$_i") - _info "Found matching record with id: $dns_record_id" - return 0 - else - _debug2 "Record does not match - type: '$_current_type' name: '$_current_name' value: '$_current_content'; Expected '$_record_name_lowercase.' '5' '$_txt_value'" - fi - _i=$(_math "$_i" + 1) - done - - _debug2 "No matching record was found in $_dns_details" - return 1 -} - -# -# Usage: _ab_dns_details domain.com -# -# Retrieve dns info for the given input domain -# -# Variables -# _domain: $1 -# dns_details: the json returned by the call to $ARUBABUSINESS_API/api/domains/dns/$_domain/details (if return status is 0) -# response -# -_ab_dns_details() { - _domain=$1 - - if ! _ab_rest GET "api/domains/dns/$_domain/details" || ! _contains "$response" "DomainId"; then - return 1 - fi - - dns_details="$response" - return 0 -} - -# -# Usage: _ab_authenticate -# -# Read account conf, update domain conf and perform user authentication to acquire an access token -# -# Variables -# AB_Key -# AB_User -# AB_Pass -# AB_Token -# -_ab_authenticate() { - AB_Key="${AB_Key:-$(_readaccountconf_mutable AB_Key)}" - AB_User="${AB_User:-$(_readaccountconf_mutable AB_User)}" - AB_Pass="${AB_Pass:-$(_readaccountconf_mutable AB_Pass)}" - - if [ -z "$AB_Key" ] || [ -z "$AB_User" ] || [ -z "$AB_Pass" ]; then - AB_Key="" - AB_User="" - AB_Pass="" - _err "Either the ArubaBusiness API key, the user or the password has not been defined yet." - _err "Please configure them and try again." - return 1 - fi - - _saveaccountconf_mutable AB_Key "$AB_Key" - _saveaccountconf_mutable AB_User "$AB_User" - _saveaccountconf_mutable AB_Pass "$AB_Pass" - - if ! _ab_get_token || [ -z "$AB_Token" ]; then - _err "Failed to acquire an access token" - return 1 - fi - - return 0 -} - -# -# Usage: _ab_get_token -# -# Try acquiring a temporary access token. The token should have a 24h lifespan -# -# Variables -# _ab_user_enc -# _ab_pass_enc -# _ab_authdata -# AB_User -# AB_Pass -# AB_Token -# response -# _H2 -# -_ab_get_token() { - _ab_user_enc=$(printf "%s" "$AB_User" | _url_encode) - _ab_pass_enc=$(printf "%s" "$AB_Pass" | _url_encode) - _ab_authdata="grant_type=password&username=$_ab_user_enc&password=$_ab_pass_enc" - - _H2="Content-Type: application/x-www-form-urlencoded" - - if ! _ab_rest POST "auth/token" "$_ab_authdata" || ! _contains "$response" "access_token"; then - _err "Authentication failure" - return 1 - fi - - AB_Token="$(printf "%s" "$response" | _egrep_o '"access_token":"[^\"]*"' | cut -d : -f 2 | tr -d '"')" - - if [ -z "$AB_Token" ]; then - _err "Could not extract access token" - return 1 - fi - - _debug "Acquired access token" - return 0 -} - -# -# Usage: _ab_rest POST "example/endpoint" "password=123" -# -# Perform a REST request using the given method, endpoint and data -# -# Variables -# _method: $1 - The http method -# _endpoint: $2 - The api path (relative to $ARUBABUSINESS_API) -# _data: $3 - The body of the request (optional) -# _key_trimmed -# _token_trimmed -# _ret_code -# AB_Key -# AB_Token -# ARUBABUSINESS_API -# _H1 -# _H2 -# _H3 -# _H4 -# -_ab_rest() { - _method=$1 - _endpoint="$2" - _data="$3" - - _key_trimmed=$(printf "%s" "$AB_Key" | tr -d '"') - _token_trimmed=$(printf "%s" "$AB_Token" | tr -d '"') - - _H1="Accept: application/json" - - if [ -z "$_H2" ]; then - # Default to application/json - _H2="Content-Type: application/json" - fi - - if [ "$_key_trimmed" ]; then - _H3="Authorization-Key: $_key_trimmed" - else - _err "Missing Api Key" - _ab_cleanup_headers - return 1 - fi - - if [ "$_token_trimmed" ]; then - _H4="Authorization: Bearer $_token_trimmed" - else - _debug "No access token set" - fi - - if [ "$_method" != "GET" ]; then - response="$(_post "$_data" "$ARUBABUSINESS_API/$_endpoint" "" "$_method")" - else - response="$(_get "$ARUBABUSINESS_API/$_endpoint")" - fi - - _ret_code=$? - - if [ "$_ret_code" = "0" ] && _ab_call_is_success; then - # Normalize the json response - response="$(printf "%s" "$response" | _normalizeJson)" - _ret_code=0 - else - _err "Failed to call endpoint: $_endpoint" - _ret_code=1 - fi - - _ab_cleanup_headers - - return $_ret_code -} - -# -# Usage: _ab_cleanup_headers -# -# Unset header variables to avoid interfering with other calls -# -# Variables -# _H1 -# _H2 -# _H3 -# _H4 -# -_ab_cleanup_headers() { - # Cleanup request headers - unset _H1 _H2 _H3 _H4 _H5 - - # Cleanup response headers - if [ -f "$HTTP_HEADER" ]; then - : >"$HTTP_HEADER" - fi -} - -# -# Usage: _ab_call_is_success -# -# Check whether a call's response http status is one of 200, 201, 202 or 204 (other 2xx are not handled) -# -# Variables -# _status -# _http_status -# _success_http_codes -# HTTP_HEADER -# -_ab_call_is_success() { - _success_http_codes="200 201 202 204" - if [ -f "$HTTP_HEADER" ]; then - _http_status=$(_egrep_o "^HTTP[\/0-9. ]*" <"$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2) - for _status in $_success_http_codes; do - if [ "$_status" = "$_http_status" ]; then - return 0 - fi - done - fi - - return 1 -} 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..b76d69c2 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" diff --git a/dnsapi/dns_baidu.sh b/dnsapi/dns_baidu.sh index dfad8eeb..8651deab 100644 --- a/dnsapi/dns_baidu.sh +++ b/dnsapi/dns_baidu.sh @@ -49,95 +49,26 @@ Options: 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" + if ! _baidu_prepare_record "$fulldomain"; then + _baidu_err "baidu_prepare_record 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 + if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then _baidu_err "baidu_find_record_ids failed for add: $_record_domain.$_zone_name" return 1 fi @@ -154,28 +85,16 @@ _baidu_add_record() { _ttl="300" ;; esac - - txtvalue="$(_baidu_trim_ws "$_txtvalue")" + _view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")" + txtvalue="$(_baidu_trim_ws "$txtvalue")" _record_domain="$(_baidu_trim_ws "$_record_domain")" _zone_name="$(_baidu_trim_ws "$_zone_name")" - 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 + _body="$(_baidu_payload_add_txt "$_zone_name" "$_record_domain" "$txtvalue" "$_ttl" "$_view")" + + if ! _baidu_bcd_post "/domain/resolve/add" "$_body"; then + _baidu_err "baidu_bcd_post failed: add record" + return 1 fi if _baidu_is_api_error "$response"; then @@ -186,10 +105,16 @@ _baidu_add_record() { return 0 } -_baidu_rm_record() { - _txtvalue="$1" +dns_baidu_rm() { + fulldomain=$(_idn "$1") + txtvalue=$2 - if ! _baidu_find_record_ids_current "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then + if ! _baidu_prepare_record "$fulldomain"; then + _baidu_err "baidu_prepare_record failed for delete: $fulldomain" + return 1 + fi + + if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then _baidu_err "baidu_find_record_ids failed for delete: $_record_domain.$_zone_name" return 1 fi @@ -213,37 +138,28 @@ _baidu_rm_record() { 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 + _body="$(_baidu_payload_delete "$_zone_name" "$_rid")" + if ! _baidu_bcd_post "/domain/resolve/delete" "$_body"; then + _baidu_err "baidu_bcd_post failed: delete recordId=$_rid" + return 1 + fi + if _baidu_is_api_error "$response"; then + _baidu_err "$response" + return 1 fi done - if [ "$_baidu_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 + if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then + _baidu_err "baidu_find_record_ids failed for delete verify: $_record_domain.$_zone_name" + return 1 + fi + _left_ids="$_BAIDU_FIND_RESULT" + if [ -z "$_left_ids" ]; then + return 0 + fi + if [ -n "$_left_ids" ]; then + _baidu_err "delete verification failed: $_record_domain.$_zone_name still has TXT records" + return 1 fi return 0 @@ -266,7 +182,6 @@ _baidu_load_credentials() { _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 @@ -274,16 +189,13 @@ _baidu_load_credentials() { _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 + if ! _baidu_load_credentials; then + _baidu_err "baidu_load_credentials failed" + return 1 + fi + if ! _baidu_get_root "$_fulldomain"; then + _baidu_err "Could not find zone for $_fulldomain" + return 1 fi _record_domain="$_sub_domain" _zone_name="$_domain" @@ -322,43 +234,6 @@ _baidu_get_root() { 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" @@ -418,39 +293,6 @@ EOF _BAIDU_FIND_RESULT="$_ids" } -_baidu_find_record_ids_dns() { - _zone_name="$1" - _record_domain="$2" - _rdtype="$3" - _rdata="$4" - _BAIDU_FIND_RESULT="" - - if ! _baidu_dns_call "GET" "/v1/dns/zone/${_zone_name}/record" ""; then - _baidu_err "baidu_dns_call failed: list records" - return 1 - fi - - if _baidu_is_api_error "$response"; then - _baidu_err "baidu_dns error: $(_baidu_json_get_str "$response" "code") $(_baidu_json_get_str "$response" "message")" - return 1 - fi - - _normalized="$(printf "%s" "$response" | _normalizeJson)" - _records=$(printf "%s" "$_normalized" | sed 's/},{/}\n{/g') - _ids="" - - while IFS= read -r _line; do - _id="$(_baidu_match_record_id_dns "$_line" "$_record_domain" "$_rdtype" "$_rdata")" - if [ "$_id" ]; then - _ids="$_ids $_id" - fi - done < ${_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_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_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_cyon.sh b/dnsapi/dns_cyon.sh index 6677b32f..d4b6b6e8 100644 --- a/dnsapi/dns_cyon.sh +++ b/dnsapi/dns_cyon.sh @@ -285,15 +285,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)" diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh index 6ad60442..f0f4c32e 100644 --- a/dnsapi/dns_czechia.sh +++ b/dnsapi/dns_czechia.sh @@ -30,9 +30,8 @@ dns_czechia_add() { 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]*\$//") + _cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') + _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') if [ -z "$_cz" ] || [ -z "$_tk" ]; then _err "Missing zone or CZ_AuthorizationToken." @@ -77,7 +76,7 @@ dns_czechia_add() { return 0 fi - _nres="$(printf '%s' "$_res" | _normalizeJson)" + _nres="$(_normalizeJson "$_res")" if [ "$?" -ne 0 ] || [ -z "$_nres" ]; then _nres="$_res" fi @@ -109,9 +108,8 @@ dns_czechia_rm() { 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]*\$//") + _cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') + _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') if [ -z "$_cz" ] || [ -z "$_tk" ]; then _err "Missing zone or CZ_AuthorizationToken." @@ -182,13 +180,12 @@ _czechia_load_conf() { } _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/\.\$//") + _clean_z=$(printf "%s" "$_z" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') [ -z "$_clean_z" ] && continue case "$_fd" in 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_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_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_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_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_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_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_freedns.sh b/dnsapi/dns_freedns.sh index 8ea86c24..13d9f68b 100755 --- a/dnsapi/dns_freedns.sh +++ b/dnsapi/dns_freedns.sh @@ -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..d598a657 100644 --- a/dnsapi/dns_freemyip.sh +++ b/dnsapi/dns_freemyip.sh @@ -6,7 +6,7 @@ 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 +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_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_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 </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 index a3d9174a..b3211069 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -6,13 +6,13 @@ 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_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api). HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds). - HOSTUP_ZONE_ID Optional. Force a specific v2 zone ID (zone_...) and skip auto-detection. + HOSTUP_ZONE_ID Optional. Force a specific zone ID (skip auto-detection). Author: HostUp (https://cloud.hostup.se/contact/en) ' -HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api/v2" +HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api" HOSTUP_DEFAULT_TTL=60 # Public: add TXT record @@ -20,7 +20,6 @@ HOSTUP_DEFAULT_TTL=60 dns_hostup_add() { fulldomain="$1" txtvalue="$2" - hostup_add_txtvalue="$2" _info "Using HostUp DNS API" @@ -35,36 +34,33 @@ dns_hostup_add() { 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")" + record_value="$(_hostup_json_escape "$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 + ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}" _debug "zone_id" "$HOSTUP_ZONE_ID" _debug "zone_domain" "$HOSTUP_ZONE_DOMAIN" _debug "record_name" "$record_name" _debug "ttl" "$ttl" - 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 + request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$record_value\",\"ttl\":$ttl}" + + if ! _hostup_rest "POST" "/dns/zones/$HOSTUP_ZONE_ID/records" "$request_body"; then + return 1 fi - 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 + if ! _contains "$_hostup_response" '"success":true'; then + _err "HostUp DNS API: failed to create TXT record for $fulldomain" + _debug2 "_hostup_response" "$_hostup_response" return 1 fi + record_id="$(_hostup_extract_record_id "$_hostup_response")" + if [ -n "$record_id" ]; then + _hostup_save_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_id" + _debug "hostup_saved_record_id" "$record_id" + fi + _info "Added TXT record for $fulldomain" return 0 } @@ -89,9 +85,20 @@ dns_hostup_rm() { record_name_fqdn="$(_hostup_fqdn "$fulldomain")" record_value="$txtvalue" + record_id_cached="$(_hostup_get_saved_record_id "$HOSTUP_ZONE_ID" "$fulldomain")" + if [ -n "$record_id_cached" ]; then + _debug "hostup_record_id_cached" "$record_id_cached" + if _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$record_id_cached"; then + _info "Deleted TXT record $record_id_cached" + _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" + HOSTUP_ZONE_ID="" + return 0 + fi + fi + if ! _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$record_value"; then _info "TXT record not found for $record_name_fqdn. Skipping removal." - _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value" + _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" return 0 fi @@ -102,7 +109,7 @@ dns_hostup_rm() { fi _info "Deleted TXT record $HOSTUP_RECORD_ID" - _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value" + _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" HOSTUP_ZONE_ID="" return 0 } @@ -120,18 +127,21 @@ _hostup_init() { 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." + _err "Please export your HostUp API key with read:dns and write:dns scopes." return 1 fi _saveaccountconf_mutable HOSTUP_API_KEY "$HOSTUP_API_KEY" _saveaccountconf_mutable HOSTUP_API_BASE "$HOSTUP_API_BASE" + if [ -n "$HOSTUP_TTL" ]; then + _saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL" + fi + if [ -n "$HOSTUP_ZONE_ID" ]; then _saveaccountconf_mutable HOSTUP_ZONE_ID "$HOSTUP_ZONE_ID" fi @@ -139,80 +149,11 @@ _hostup_init() { 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="" + return 0 fi HOSTUP_ZONE_DOMAIN="" @@ -221,16 +162,16 @@ _hostup_detect_zone() { 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" + return 0 fi HOSTUP_ZONE_ID="" - HOSTUP_ZONE_DOMAIN="" fi - _domain_candidate="$(printf "%s" "${fulldomain%.}" | _lower_case)" + if ! _hostup_load_zones; then + return 1 + fi + + _domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)" _debug "hostup_initial_candidate" "$_domain_candidate" while [ -n "$_domain_candidate" ]; do @@ -299,11 +240,11 @@ _hostup_fqdn() { _hostup_fetch_zone_details() { zone_id="$1" - if ! _hostup_rest "GET" "/dns-zones/$zone_id/records" ""; then + if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then return 1 fi - zonedomain="$(_hostup_json_extract "name" "$_hostup_response")" + zonedomain="$(printf "%s" "$_hostup_response" | _egrep_o '"domain":"[^"]*"' | sed -n '1p' | cut -d ':' -f 2 | tr -d '"')" if [ -n "$zonedomain" ]; then HOSTUP_ZONE_DOMAIN="$zonedomain" return 0 @@ -313,7 +254,7 @@ _hostup_fetch_zone_details() { } _hostup_load_zones() { - if ! _hostup_rest "GET" "/dns-zones?limit=1000" ""; then + if ! _hostup_rest "GET" "/dns/zones" ""; then return 1 fi @@ -322,9 +263,9 @@ _hostup_load_zones() { 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")" + *'"domain_id"'*'"domain"'*) + zone_id="$(printf "%s" "$line" | _hostup_json_extract "domain_id")" + zone_domain="$(printf "%s" "$line" | _hostup_json_extract "domain")" if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id} " @@ -349,30 +290,9 @@ _hostup_lookup_zone() { _lookup_zone_id="" _lookup_zone_domain="" - encoded_domain="$(printf "%s" "$lookup_domain" | _url_encode)" - if _hostup_rest "GET" "/dns-zones?name=$encoded_domain&limit=1" ""; then - zone_id="$(_hostup_json_extract "id" "$_hostup_response")" - zone_domain="$(_hostup_json_extract "name" "$_hostup_response")" - if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then - zone_domain_lower="$(printf "%s" "$zone_domain" | _lower_case)" - if [ "$zone_domain_lower" = "$lookup_domain" ]; then - _lookup_zone_domain="$zone_domain" - _lookup_zone_id="$zone_id" - HOSTUP_ZONE_DOMAIN="$zone_domain" - HOSTUP_ZONE_ID="$zone_id" - return 0 - fi - fi - fi - - if [ -z "$HOSTUP_ZONES_CACHE" ] && ! _hostup_load_zones; then - return 1 - fi - while IFS='|' read -r domain zone_id; do [ -z "$domain" ] && continue - domain_lower="$(printf "%s" "$domain" | _lower_case)" - if [ "$domain_lower" = "$lookup_domain" ]; then + if [ "$domain" = "$lookup_domain" ]; then _lookup_zone_domain="$domain" _lookup_zone_id="$zone_id" HOSTUP_ZONE_DOMAIN="$domain" @@ -387,50 +307,50 @@ EOF } _hostup_find_record() { - _hostup_find_zone_id="$1" - _hostup_find_fqdn="$2" - _hostup_find_txtvalue="$3" + zone_id="$1" + fqdn="$2" + txtvalue="$3" - _hostup_find_encoded_name="$(printf "%s" "$_hostup_find_fqdn" | _url_encode)" - if ! _hostup_rest "GET" "/dns-zones/$_hostup_find_zone_id/records?type=TXT&name=$_hostup_find_encoded_name" ""; then + if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then return 1 fi HOSTUP_RECORD_ID="" - _hostup_find_records="$(printf "%s" "$_hostup_response" | tr '{' '\n')" + records="$(printf "%s" "$_hostup_response" | tr '{' '\n')" - while IFS= read -r _hostup_find_line; do + while IFS= read -r line; do # Normalize line to make TXT value matching reliable - _hostup_find_line_clean="$(printf "%s" "$_hostup_find_line" | tr -d '\r\n')" - _hostup_find_line_value_clean="$(printf "%s" "$_hostup_find_line_clean" | sed 's/\\"//g')" + line_clean="$(printf "%s" "$line" | tr -d '\r\n')" + line_value_clean="$(printf "%s" "$line_clean" | sed 's/\\"//g')" - _hostup_find_record_type="$(_hostup_json_extract "type" "$_hostup_find_line_clean")" - [ "$_hostup_find_record_type" != "TXT" ] && continue + case "$line_clean" in + *'"type":"TXT"'*'"name"'*'"value"'*) + name_value="$(_hostup_json_extract "name" "$line_clean")" + record_value="$(_hostup_json_extract "value" "$line_value_clean")" - _hostup_find_name_value="$(_hostup_json_extract "name" "$_hostup_find_line_clean")" - _hostup_find_record_value="$(_hostup_json_extract "value" "$_hostup_find_line_value_clean")" - - _debug "hostup_record_raw" "$_hostup_find_record_value" - if [ "${_hostup_find_record_value#\"}" != "$_hostup_find_record_value" ] && [ "${_hostup_find_record_value%\"}" != "$_hostup_find_record_value" ]; then - _hostup_find_record_value="${_hostup_find_record_value#\"}" - _hostup_find_record_value="${_hostup_find_record_value%\"}" - fi - if [ "${_hostup_find_record_value#\'}" != "$_hostup_find_record_value" ] && [ "${_hostup_find_record_value%\'}" != "$_hostup_find_record_value" ]; then - _hostup_find_record_value="${_hostup_find_record_value#\'}" - _hostup_find_record_value="${_hostup_find_record_value%\'}" - fi - _hostup_find_record_value="$(printf "%s" "$_hostup_find_record_value" | tr -d '\r\n')" - _debug "hostup_record_value" "$_hostup_find_record_value" - - if [ "$_hostup_find_name_value" = "$_hostup_find_fqdn" ] && [ "$_hostup_find_record_value" = "$_hostup_find_txtvalue" ]; then - _hostup_find_record_id="$(_hostup_json_extract "id" "$_hostup_find_line_clean")" - if [ -n "$_hostup_find_record_id" ]; then - HOSTUP_RECORD_ID="$_hostup_find_record_id" - return 0 + _debug "hostup_record_raw" "$record_value" + if [ "${record_value#\"}" != "$record_value" ] && [ "${record_value%\"}" != "$record_value" ]; then + record_value="${record_value#\"}" + record_value="${record_value%\"}" fi - fi + if [ "${record_value#\'}" != "$record_value" ] && [ "${record_value%\'}" != "$record_value" ]; then + record_value="${record_value#\'}" + record_value="${record_value%\'}" + fi + record_value="$(printf "%s" "$record_value" | tr -d '\r\n')" + _debug "hostup_record_value" "$record_value" + + if [ "$name_value" = "$fqdn" ] && [ "$record_value" = "$txtvalue" ]; then + record_id="$(_hostup_json_extract "id" "$line_clean")" + if [ -n "$record_id" ]; then + HOSTUP_RECORD_ID="$record_id" + return 0 + fi + fi + ;; + esac done <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 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..401471be 100644 --- a/dnsapi/dns_joker.sh +++ b/dnsapi/dns_joker.sh @@ -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_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..163fe8db 100644 --- a/dnsapi/dns_me.sh +++ b/dnsapi/dns_me.sh @@ -140,7 +140,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_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_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..1062c849 100755 --- a/dnsapi/dns_namecom.sh +++ b/dnsapi/dns_namecom.sh @@ -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..5d47a59a 100755 --- a/dnsapi/dns_namesilo.sh +++ b/dnsapi/dns_namesilo.sh @@ -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_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_oci.sh b/dnsapi/dns_oci.sh index e1aa3dd9..c76a4565 100644 --- a/dnsapi/dns_oci.sh +++ b/dnsapi/dns_oci.sh @@ -115,15 +115,12 @@ _oci_config() { _clearaccountconf_mutable OCI_CLI_PROFILE fi - if [ -z "$OCI_CLI_TENANCY" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then - _debug "Reading OCI_CLI_TENANCY value from: $OCI_CLI_CONFIG_FILE" - OCI_CLI_TENANCY=$(_readini "$OCI_CLI_CONFIG_FILE" tenancy "$OCI_CLI_PROFILE") - fi - if [ -z "$OCI_CLI_TENANCY" ]; then - OCI_CLI_TENANCY=$(_readaccountconf_mutable OCI_CLI_TENANCY) - fi + OCI_CLI_TENANCY="${OCI_CLI_TENANCY:-$(_readaccountconf_mutable OCI_CLI_TENANCY)}" if [ "$OCI_CLI_TENANCY" ]; then _saveaccountconf_mutable OCI_CLI_TENANCY "$OCI_CLI_TENANCY" + elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then + _debug "Reading OCI_CLI_TENANCY value from: $OCI_CLI_CONFIG_FILE" + OCI_CLI_TENANCY="${OCI_CLI_TENANCY:-$(_readini "$OCI_CLI_CONFIG_FILE" tenancy "$OCI_CLI_PROFILE")}" fi if [ -z "$OCI_CLI_TENANCY" ]; then @@ -131,47 +128,41 @@ _oci_config() { return 1 fi - if [ -z "$OCI_CLI_USER" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then - _debug "Reading OCI_CLI_USER value from: $OCI_CLI_CONFIG_FILE" - OCI_CLI_USER=$(_readini "$OCI_CLI_CONFIG_FILE" user "$OCI_CLI_PROFILE") - fi - if [ -z "$OCI_CLI_USER" ]; then - OCI_CLI_USER=$(_readaccountconf_mutable OCI_CLI_USER) - fi + OCI_CLI_USER="${OCI_CLI_USER:-$(_readaccountconf_mutable OCI_CLI_USER)}" if [ "$OCI_CLI_USER" ]; then _saveaccountconf_mutable OCI_CLI_USER "$OCI_CLI_USER" + elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then + _debug "Reading OCI_CLI_USER value from: $OCI_CLI_CONFIG_FILE" + OCI_CLI_USER="${OCI_CLI_USER:-$(_readini "$OCI_CLI_CONFIG_FILE" user "$OCI_CLI_PROFILE")}" fi if [ -z "$OCI_CLI_USER" ]; then _err "Error: unable to read OCI_CLI_USER from config file or environment variable." return 1 fi - if [ -z "$OCI_CLI_REGION" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then - _debug "Reading OCI_CLI_REGION value from: $OCI_CLI_CONFIG_FILE" - OCI_CLI_REGION=$(_readini "$OCI_CLI_CONFIG_FILE" region "$OCI_CLI_PROFILE") - fi - if [ -z "$OCI_CLI_REGION" ]; then - OCI_CLI_REGION=$(_readaccountconf_mutable OCI_CLI_REGION) - fi + OCI_CLI_REGION="${OCI_CLI_REGION:-$(_readaccountconf_mutable OCI_CLI_REGION)}" if [ "$OCI_CLI_REGION" ]; then _saveaccountconf_mutable OCI_CLI_REGION "$OCI_CLI_REGION" + elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then + _debug "Reading OCI_CLI_REGION value from: $OCI_CLI_CONFIG_FILE" + OCI_CLI_REGION="${OCI_CLI_REGION:-$(_readini "$OCI_CLI_CONFIG_FILE" region "$OCI_CLI_PROFILE")}" fi if [ -z "$OCI_CLI_REGION" ]; then _err "Error: unable to read OCI_CLI_REGION from config file or environment variable." return 1 fi - if [ -z "$OCI_CLI_KEY_FILE" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then - OCI_CLI_KEY_FILE=$(_readini "$OCI_CLI_CONFIG_FILE" key_file "$OCI_CLI_PROFILE") - fi - if [ "$OCI_CLI_KEY" ]; then - _saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY" - elif [ "$OCI_CLI_KEY_FILE" ] && [ -f "$OCI_CLI_KEY_FILE" ]; then - _debug "Reading OCI_CLI_KEY value from: $OCI_CLI_KEY_FILE" - OCI_CLI_KEY=$(_base64 <"$OCI_CLI_KEY_FILE") - _saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY" + OCI_CLI_KEY="${OCI_CLI_KEY:-$(_readaccountconf_mutable OCI_CLI_KEY)}" + if [ -z "$OCI_CLI_KEY" ]; then + _clearaccountconf_mutable OCI_CLI_KEY + OCI_CLI_KEY_FILE="${OCI_CLI_KEY_FILE:-$(_readini "$OCI_CLI_CONFIG_FILE" key_file "$OCI_CLI_PROFILE")}" + if [ "$OCI_CLI_KEY_FILE" ] && [ -f "$OCI_CLI_KEY_FILE" ]; then + _debug "Reading OCI_CLI_KEY value from: $OCI_CLI_KEY_FILE" + OCI_CLI_KEY=$(_base64 <"$OCI_CLI_KEY_FILE") + _saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY" + fi else - OCI_CLI_KEY=$(_readaccountconf_mutable OCI_CLI_KEY) + _saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY" fi if [ -z "$OCI_CLI_KEY_FILE" ] && [ -z "$OCI_CLI_KEY" ]; then diff --git a/dnsapi/dns_ovh.sh b/dnsapi/dns_ovh.sh index df2b184d..9f2cd23f 100755 --- a/dnsapi/dns_ovh.sh +++ b/dnsapi/dns_ovh.sh @@ -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..465bcc60 100644 --- a/dnsapi/dns_pleskxml.sh +++ b/dnsapi/dns_pleskxml.sh @@ -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_rage4.sh b/dnsapi/dns_rage4.sh index b9abff17..c27fbc5f 100755 --- a/dnsapi/dns_rage4.sh +++ b/dnsapi/dns_rage4.sh @@ -71,7 +71,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') + _record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -rn 's/.*"id":([[:digit:]]+),.*/\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 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_selectel.sh b/dnsapi/dns_selectel.sh index 8ba9a4fb..565f541b 100644 --- a/dnsapi/dns_selectel.sh +++ b/dnsapi/dns_selectel.sh @@ -368,7 +368,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_udr.sh b/dnsapi/dns_udr.sh index dbc959d6..656a0557 100644 --- a/dnsapi/dns_udr.sh +++ b/dnsapi/dns_udr.sh @@ -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_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_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..f59715ac 100644 --- a/dnsapi/dns_world4you.sh +++ b/dnsapi/dns_world4you.sh @@ -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 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g') if [ "$msg" = '' ]; then _err "Unable to add record: Unknown error" echo "$ret" >'error-01.html' @@ -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 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g') if [ "$msg" = '' ]; then _err "Unable to remove record: Unknown error" echo "$ret" >'error-01.html' @@ -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/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/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 -}