diff --git a/.github/auto-comment.yml b/.github/auto-comment.yml deleted file mode 100644 index 520b3ce3..00000000 --- a/.github/auto-comment.yml +++ /dev/null @@ -1,40 +0,0 @@ -# Comment to a new issue. -issuesOpened: > - If this is a bug report, please upgrade to the latest code and try again: - - 如果有 bug, 请先更新到最新版试试: - - ``` - acme.sh --upgrade - ``` - - please also provide the log with `--debug 2`. - - 同时请提供调试输出 `--debug 2` - - see: https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh - - Without `--debug 2` log, your issue will NEVER get replied. - - 没有调试输出, 你的 issue 不会得到任何解答. - - -pullRequestOpened: > - First, NEVER send a PR to `master` branch, it will NEVER be accepted. Please send to the `dev` branch instead. - - If this is a PR to support new DNS API or new notification API, please read this guide first: - https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide - - Please check the guide items one by one. - - Then add your usage here: - https://github.com/acmesh-official/acme.sh/wiki/dnsapi - - Or some other wiki pages: - - https://github.com/acmesh-official/acme.sh/wiki/deployhooks - - https://github.com/acmesh-official/acme.sh/wiki/notify - - - diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 5dc2d453..56781fff 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -59,24 +59,24 @@ jobs: run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file run: | - cd ../acmetest + cd ../acmetest if [ "${{ secrets.TokenName1}}" ] ; then - echo "${{ secrets.TokenName1}}=${{ secrets.TokenValue1}}" >> env.list + echo "${{ secrets.TokenName1}}=${{ secrets.TokenValue1}}" >> docker.env fi if [ "${{ secrets.TokenName2}}" ] ; then - echo "${{ secrets.TokenName2}}=${{ secrets.TokenValue2}}" >> env.list + echo "${{ secrets.TokenName2}}=${{ secrets.TokenValue2}}" >> docker.env fi if [ "${{ secrets.TokenName3}}" ] ; then - echo "${{ secrets.TokenName3}}=${{ secrets.TokenValue3}}" >> env.list + echo "${{ secrets.TokenName3}}=${{ secrets.TokenValue3}}" >> docker.env fi if [ "${{ secrets.TokenName4}}" ] ; then - echo "${{ secrets.TokenName4}}=${{ secrets.TokenValue4}}" >> env.list + echo "${{ secrets.TokenName4}}=${{ secrets.TokenValue4}}" >> docker.env fi if [ "${{ secrets.TokenName5}}" ] ; then - echo "${{ secrets.TokenName5}}=${{ secrets.TokenValue5}}" >> env.list + echo "${{ secrets.TokenName5}}=${{ secrets.TokenValue5}}" >> docker.env fi - echo "TEST_DNS_NO_WILDCARD" >> env.list - echo "TEST_DNS_SLEEP" >> env.list + echo "TEST_DNS_NO_WILDCARD" >> docker.env + echo "TEST_DNS_SLEEP" >> docker.env - name: Run acmetest run: cd ../acmetest && ./rundocker.sh testall @@ -170,7 +170,7 @@ jobs: ./letest.sh FreeBSD: - runs-on: macos-latest + runs-on: macos-10.15 needs: Windows env: TEST_DNS : ${{ secrets.TEST_DNS }} @@ -184,7 +184,7 @@ jobs: - uses: actions/checkout@v2 - name: Clone acmetest run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - uses: vmactions/freebsd-vm@v0.0.7 + - uses: vmactions/freebsd-vm@v0.1.4 with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: pkg install -y socat curl @@ -209,7 +209,7 @@ jobs: ./letest.sh Solaris: - runs-on: macos-latest + runs-on: macos-10.15 needs: FreeBSD env: TEST_DNS : ${{ secrets.TEST_DNS }} @@ -223,11 +223,13 @@ jobs: - uses: actions/checkout@v2 - name: Clone acmetest run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - uses: vmactions/solaris-vm@v0.0.1 + - uses: vmactions/solaris-vm@v0.0.3 with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' - prepare: pkgutil -y -i socat curl + prepare: pkgutil -y -i socat run: | + pkg set-mediator -v -I default@1.1 openssl + export PATH=/usr/gnu/bin:$PATH if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}=${{ secrets.TokenValue1}} fi @@ -245,5 +247,3 @@ jobs: fi cd ../acmetest ./letest.sh - - diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml new file mode 100644 index 00000000..5d032769 --- /dev/null +++ b/.github/workflows/FreeBSD.yml @@ -0,0 +1,63 @@ +name: FreeBSD +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/FreeBSD.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/FreeBSD.yml' + + +jobs: + FreeBSD: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + - TEST_ACME_Server: "ZeroSSL.com" + CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + CA: "ZeroSSL RSA Domain Secure Site CA" + CA_EMAIL: "githubtest@acme.sh" + TEST_PREFERRED_CHAIN: "" + runs-on: macos-10.15 + 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@v2 + - uses: vmactions/cf-tunnel@v0.0.3 + 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 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/freebsd-vm@v0.1.5 + with: + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN' + nat: | + "8080": "80" + prepare: pkg install -y socat curl + usesh: true + run: | + cd ../acmetest \ + && ./letest.sh + + diff --git a/.github/workflows/LetsEncrypt.yml b/.github/workflows/LetsEncrypt.yml deleted file mode 100644 index 8d0c4eb0..00000000 --- a/.github/workflows/LetsEncrypt.yml +++ /dev/null @@ -1,147 +0,0 @@ -name: LetsEncrypt -on: - push: - branches: - - '*' - paths: - - '**.sh' - - '**.yml' - pull_request: - branches: - - dev - paths: - - '**.sh' - - '**.yml' - - -jobs: - CheckToken: - runs-on: ubuntu-latest - outputs: - hasToken: ${{ steps.step_one.outputs.hasToken }} - env: - NGROK_TOKEN : ${{ secrets.NGROK_TOKEN }} - steps: - - name: Set the value - id: step_one - run: | - if [ "$NGROK_TOKEN" ] ; then - echo "::set-output name=hasToken::true" - else - echo "::set-output name=hasToken::false" - fi - - name: Check the value - run: echo ${{ steps.step_one.outputs.hasToken }} - - Ubuntu: - runs-on: ubuntu-latest - needs: CheckToken - if: "contains(needs.CheckToken.outputs.hasToken, 'true')" - env: - NGROK_TOKEN : ${{ secrets.NGROK_TOKEN }} - TEST_LOCAL: 1 - steps: - - uses: actions/checkout@v2 - - name: Install tools - run: sudo apt-get install -y socat - - name: Clone acmetest - run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Run acmetest - run: cd ../acmetest && sudo --preserve-env ./letest.sh - - MacOS: - runs-on: macos-latest - needs: Ubuntu - env: - NGROK_TOKEN : ${{ secrets.NGROK_TOKEN }} - TEST_LOCAL: 1 - steps: - - uses: actions/checkout@v2 - - name: Install tools - run: brew install socat - - name: Clone acmetest - run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Run acmetest - run: cd ../acmetest && sudo --preserve-env ./letest.sh - - Windows: - runs-on: windows-latest - needs: MacOS - env: - NGROK_TOKEN : ${{ secrets.NGROK_TOKEN }} - TEST_LOCAL: 1 - #The 80 port is used by Windows server, we have to use a custom port, ngrok will also use this port. - Le_HTTPPort: 8888 - steps: - - name: Set git to use LF - run: | - git config --global core.autocrlf false - - uses: actions/checkout@v2 - - name: Install cygwin base packages with chocolatey - run: | - choco config get cacheLocation - choco install --no-progress cygwin - shell: cmd - - name: Install cygwin additional packages - run: | - C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s http://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git - shell: cmd - - name: Set ENV - shell: cmd - run: | - echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin >> %GITHUB_ENV% - - name: Check ENV - shell: cmd - run: | - echo "PATH=%PATH%" - - name: Clone acmetest - shell: cmd - run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Run acmetest - shell: cmd - run: cd ../acmetest && bash.exe -c ./letest.sh - - FreeBSD: - runs-on: macos-latest - needs: Windows - env: - NGROK_TOKEN : ${{ secrets.NGROK_TOKEN }} - TEST_LOCAL: 1 - steps: - - uses: actions/checkout@v2 - - name: Clone acmetest - run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - uses: vmactions/freebsd-vm@v0.0.7 - with: - envs: 'NGROK_TOKEN TEST_LOCAL' - prepare: pkg install -y socat curl - usesh: true - run: | - cd ../acmetest && ./letest.sh - - Solaris: - runs-on: macos-latest - needs: FreeBSD - env: - NGROK_TOKEN : ${{ secrets.NGROK_TOKEN }} - TEST_LOCAL: 1 - steps: - - uses: actions/checkout@v2 - - uses: vmactions/ngrok-tunnel@v0.0.1 - id: ngrok - with: - protocol: http - port: 8080 - - name: Set envs - run: echo "TestingDomain=${{steps.ngrok.outputs.server}}" >> $GITHUB_ENV - - name: Clone acmetest - run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - uses: vmactions/solaris-vm@v0.0.1 - with: - envs: 'TEST_LOCAL TestingDomain' - nat: | - "8080": "80" - prepare: pkgutil -y -i socat curl - run: | - cd ../acmetest && ./letest.sh - diff --git a/.github/workflows/Linux.yml b/.github/workflows/Linux.yml new file mode 100644 index 00000000..cba708b3 --- /dev/null +++ b/.github/workflows/Linux.yml @@ -0,0 +1,41 @@ +name: Linux +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Linux.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Linux.yml' + + + +jobs: + Linux: + strategy: + matrix: + os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "centos:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "mageia", "gentoo/stage3-amd64"] + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + steps: + - uses: actions/checkout@v2 + - name: Clone acmetest + run: | + cd .. \ + && git clone https://github.com/acmesh-official/acmetest.git \ + && cp -r acme.sh acmetest/ + - name: Run acmetest + run: | + cd ../acmetest \ + && ./rundocker.sh testplat ${{ matrix.os }} + + + diff --git a/.github/workflows/MacOS.yml b/.github/workflows/MacOS.yml new file mode 100644 index 00000000..4b529f6a --- /dev/null +++ b/.github/workflows/MacOS.yml @@ -0,0 +1,55 @@ +name: MacOS +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/MacOS.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/MacOS.yml' + + +jobs: + MacOS: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + - TEST_ACME_Server: "ZeroSSL.com" + CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + CA: "ZeroSSL RSA Domain Secure Site CA" + CA_EMAIL: "githubtest@acme.sh" + TEST_PREFERRED_CHAIN: "" + runs-on: macos-latest + 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@v2 + - name: Install tools + run: brew install socat + - name: Clone acmetest + run: | + cd .. \ + && git clone 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/PebbleStrict.yml b/.github/workflows/PebbleStrict.yml index 976e5373..c1ea1cd2 100644 --- a/.github/workflows/PebbleStrict.yml +++ b/.github/workflows/PebbleStrict.yml @@ -4,14 +4,14 @@ on: branches: - '*' paths: - - '**.sh' - - '**.yml' + - '*.sh' + - '.github/workflows/PebbleStrict.yml' pull_request: branches: - dev paths: - - '**.sh' - - '**.yml' + - '*.sh' + - '.github/workflows/PebbleStrict.yml' jobs: PebbleStrict: @@ -19,7 +19,7 @@ jobs: env: TestingDomain: example.com TestingAltDomains: www.example.com - ACME_DIRECTORY: https://localhost:14000/dir + TEST_ACME_Server: https://localhost:14000/dir HTTPS_INSECURE: 1 Le_HTTPPort: 5002 TEST_LOCAL: 1 @@ -35,5 +35,28 @@ jobs: run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4 - name: Clone acmetest run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Run acmetest + run: cd ../acmetest && ./letest.sh + + PebbleStrict_IPCert: + runs-on: ubuntu-latest + env: + TestingDomain: 10.30.50.1 + ACME_DIRECTORY: https://localhost:14000/dir + HTTPS_INSECURE: 1 + Le_HTTPPort: 5002 + Le_TLSPort: 5001 + TEST_LOCAL: 1 + TEST_CA: "Pebble Intermediate CA" + TEST_IPCERT: 1 + + steps: + - uses: actions/checkout@v2 + - name: Install tools + run: sudo apt-get install -y socat + - name: Run Pebble + run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker-compose up -d + - name: Clone acmetest + run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Run acmetest run: cd ../acmetest && ./letest.sh \ No newline at end of file diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml new file mode 100644 index 00000000..4df10099 --- /dev/null +++ b/.github/workflows/Solaris.yml @@ -0,0 +1,61 @@ +name: Solaris +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Solaris.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Solaris.yml' + + +jobs: + Solaris: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + - TEST_ACME_Server: "ZeroSSL.com" + CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + CA: "ZeroSSL RSA Domain Secure Site CA" + CA_EMAIL: "githubtest@acme.sh" + TEST_PREFERRED_CHAIN: "" + runs-on: macos-10.15 + 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@v2 + - uses: vmactions/cf-tunnel@v0.0.3 + 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 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/solaris-vm@v0.0.3 + with: + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN' + nat: | + "8080": "80" + prepare: pkgutil -y -i socat curl + run: | + cd ../acmetest \ + && ./letest.sh + diff --git a/.github/workflows/Ubuntu.yml b/.github/workflows/Ubuntu.yml new file mode 100644 index 00000000..28b06541 --- /dev/null +++ b/.github/workflows/Ubuntu.yml @@ -0,0 +1,57 @@ +name: Ubuntu +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Ubuntu.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Ubuntu.yml' + + +jobs: + Ubuntu: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + - TEST_ACME_Server: "ZeroSSL.com" + CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + CA: "ZeroSSL RSA Domain Secure Site CA" + CA_EMAIL: "githubtest@acme.sh" + TEST_PREFERRED_CHAIN: "" + + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + NO_ECC_384: ${{ matrix.NO_ECC_384 }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + steps: + - uses: actions/checkout@v2 + - name: Install tools + run: sudo apt-get install -y socat + - name: Clone acmetest + run: | + cd .. \ + && git clone 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/Windows.yml b/.github/workflows/Windows.yml new file mode 100644 index 00000000..2d7eeeae --- /dev/null +++ b/.github/workflows/Windows.yml @@ -0,0 +1,73 @@ +name: Windows +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Windows.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Windows.yml' + + +jobs: + Windows: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + - TEST_ACME_Server: "ZeroSSL.com" + CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + CA: "ZeroSSL RSA Domain Secure Site CA" + CA_EMAIL: "githubtest@acme.sh" + TEST_PREFERRED_CHAIN: "" + runs-on: windows-latest + env: + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_LOCAL: 1 + #The 80 port is used by Windows server, we have to use a custom port, tunnel will also use this port. + Le_HTTPPort: 8888 + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + steps: + - name: Set git to use LF + run: | + git config --global core.autocrlf false + - uses: actions/checkout@v2 + - name: Install cygwin base packages with chocolatey + run: | + choco config get cacheLocation + choco install --no-progress cygwin + shell: cmd + - name: Install cygwin additional packages + run: | + C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s http://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git,xxd + shell: cmd + - name: Set ENV + shell: cmd + run: | + echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin;%PATH% >> %GITHUB_ENV% + - name: Check ENV + shell: cmd + run: | + echo "PATH=%PATH%" + - name: Clone acmetest + shell: cmd + run: cd .. && git clone https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Run acmetest + shell: cmd + run: cd ../acmetest && bash.exe -c ./letest.sh + + + diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 238fde3a..0c3aec0a 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -6,6 +6,11 @@ on: - '*' tags: - '*' + paths: + - '**.sh' + - "Dockerfile" + - '.github/workflows/dockerhub.yml' + jobs: CheckToken: diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index b22a2fd8..940a187d 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -5,13 +5,13 @@ on: - '*' paths: - '**.sh' - - '**.yml' + - '.github/workflows/shellcheck.yml' pull_request: branches: - dev paths: - '**.sh' - - '**.yml' + - '.github/workflows/shellcheck.yml' jobs: ShellCheck: diff --git a/Dockerfile b/Dockerfile index 4618efaf..fb842c83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ FROM alpine:3.12 -RUN apk update -f \ - && apk --no-cache add -f \ +RUN apk --no-cache add -f \ openssl \ openssh-client \ coreutils \ @@ -12,8 +11,7 @@ RUN apk update -f \ tzdata \ oath-toolkit-oathtool \ tar \ - libidn \ - && rm -rf /var/cache/apk/* + libidn ENV LE_CONFIG_HOME /acme.sh @@ -22,7 +20,7 @@ ARG AUTO_UPGRADE=1 ENV AUTO_UPGRADE $AUTO_UPGRADE #Install -ADD ./ /install_acme.sh/ +COPY ./ /install_acme.sh/ RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/ @@ -57,6 +55,7 @@ RUN for verb in help \ deactivate-account \ set-notify \ set-default-ca \ + set-default-chain \ ; do \ printf -- "%b" "#!/usr/bin/env sh\n/root/.acme.sh/acme.sh --${verb} --config-home /acme.sh \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \ ; done diff --git a/README.md b/README.md index edd6442f..91a18985 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # An ACME Shell script: acme.sh -![LetsEncrypt](https://github.com/acmesh-official/acme.sh/workflows/LetsEncrypt/badge.svg) +[![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml) +[![MacOS](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml) +[![Ubuntu](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml) +[![Windows](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml) +[![Solaris](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml) + ![Shellcheck](https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg) ![PebbleStrict](https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg) ![DockerHub](https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg) @@ -15,18 +20,18 @@ - An ACME protocol client written purely in Shell (Unix shell) language. - Full ACME protocol implementation. -- Support ACME v1 and ACME v2 -- Support ACME v2 wildcard certs +- Support ECDSA certs +- Support SAN and wildcard certs - Simple, powerful and very easy to use. You only need 3 minutes to learn it. - Bash, dash and sh compatible. -- Purely written in Shell with no dependencies on python or the official Let's Encrypt client. +- Purely written in Shell with no dependencies on python. - Just one script to issue, renew and install your certificates automatically. - DOES NOT require `root/sudoer` access. -- Docker friendly -- IPv6 support +- Docker ready +- IPv6 ready - Cron job notifications for renewal or error etc. -It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates from Let's Encrypt. +It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates. Wiki: https://github.com/acmesh-official/acme.sh/wiki @@ -57,37 +62,39 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa) | NO | Status| Platform| |----|-------|---------| -|1|[![MacOS](https://github.com/acmesh-official/acme.sh/workflows/LetsEncrypt/badge.svg)](https://github.com/acmesh-official/acme.sh/actions?query=workflow%3ALetsEncrypt)|Mac OSX -|2|[![Windows](https://github.com/acmesh-official/acme.sh/workflows/LetsEncrypt/badge.svg)](https://github.com/acmesh-official/acme.sh/actions?query=workflow%3ALetsEncrypt)|Windows (cygwin with curl, openssl and crontab included) -|3|[![FreeBSD](https://github.com/acmesh-official/acme.sh/workflows/LetsEncrypt/badge.svg)](https://github.com/acmesh-official/acme.sh/actions?query=workflow%3ALetsEncrypt)|FreeBSD -|4|[![Solaris](https://github.com/acmesh-official/acme.sh/workflows/LetsEncrypt/badge.svg)](https://github.com/acmesh-official/acme.sh/actions?query=workflow%3ALetsEncrypt)|Solaris -|5|[![Ubuntu](https://github.com/acmesh-official/acme.sh/workflows/LetsEncrypt/badge.svg)](https://github.com/acmesh-official/acme.sh/actions?query=workflow%3ALetsEncrypt)| Ubuntu -|6|[![](https://acmesh-official.github.io/acmetest/status/pfsense.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|pfsense -|7|[![](https://acmesh-official.github.io/acmetest/status/openbsd.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|OpenBSD -|8|[![](https://acmesh-official.github.io/acmetest/status/debian-latest.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)| Debian -|9|[![](https://acmesh-official.github.io/acmetest/status/centos-latest.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|CentOS -|10|[![](https://acmesh-official.github.io/acmetest/status/opensuse-leap-latest.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|openSUSE -|11|[![](https://acmesh-official.github.io/acmetest/status/alpine-latest.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|Alpine Linux (with curl) -|12|[![](https://acmesh-official.github.io/acmetest/status/archlinux-latest.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|Archlinux -|13|[![](https://acmesh-official.github.io/acmetest/status/fedora-latest.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|fedora -|14|[![](https://acmesh-official.github.io/acmetest/status/kalilinux-kali.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|Kali Linux -|15|[![](https://acmesh-official.github.io/acmetest/status/oraclelinux-latest.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|Oracle Linux -|16|[![](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) -|17|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 -|18|[![](https://acmesh-official.github.io/acmetest/status/mageia.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|Mageia -|19|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) -|20|[![](https://acmesh-official.github.io/acmetest/status/gentoo-stage3-amd64.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|Gentoo Linux -|21|[![](https://acmesh-official.github.io/acmetest/status/clearlinux-latest.svg)](https://github.com/acmesh-official/acmetest#here-are-the-latest-status)|ClearLinux +|1|[![MacOS](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml)|Mac OSX +|2|[![Windows](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml)|Windows (cygwin with curl, openssl and crontab included) +|3|[![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml)|FreeBSD +|4|[![Solaris](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml)|Solaris +|5|[![Ubuntu](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml)| Ubuntu +|6|NA|pfsense +|7|NA|OpenBSD +|8|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian +|9|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|CentOS +|10|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE +|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl) +|12|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux +|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora +|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux +|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux +|16|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia +|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux +|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|ClearLinux +|19|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 +|20|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) +|21|[![](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) -For all build statuses, check our [weekly build project](https://github.com/acmesh-official/acmetest): + +Check our [testing project](https://github.com/acmesh-official/acmetest): https://github.com/acmesh-official/acmetest # Supported CA -- Letsencrypt.org CA(default) -- [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA) +- [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA)(default) +- Letsencrypt.org CA - [BuyPass.com CA](https://github.com/acmesh-official/acme.sh/wiki/BuyPass.com-CA) +- [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) - [Pebble strict Mode](https://github.com/letsencrypt/pebble) - Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA @@ -469,7 +476,7 @@ TODO: ### Code Contributors -This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. +This project exists thanks to all the people who contribute. ### Financial Contributors diff --git a/acme.sh b/acme.sh index a1ad4195..d2e8b04d 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=2.8.9 +VER=3.0.2 PROJECT_NAME="acme.sh" @@ -20,8 +20,7 @@ _SUB_FOLDER_DEPLOY="deploy" _SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY" -LETSENCRYPT_CA_V1="https://acme-v01.api.letsencrypt.org/directory" -LETSENCRYPT_STAGING_CA_V1="https://acme-staging.api.letsencrypt.org/directory" +CA_LETSENCRYPT_V1="https://acme-v01.api.letsencrypt.org/directory" CA_LETSENCRYPT_V2="https://acme-v02.api.letsencrypt.org/directory" CA_LETSENCRYPT_V2_TEST="https://acme-staging-v02.api.letsencrypt.org/directory" @@ -32,18 +31,22 @@ CA_BUYPASS_TEST="https://api.test4.buypass.no/acme/directory" CA_ZEROSSL="https://acme.zerossl.com/v2/DV90" _ZERO_EAB_ENDPOINT="http://api.zerossl.com/acme/eab-credentials-email" -DEFAULT_CA=$CA_LETSENCRYPT_V2 +CA_SSLCOM_RSA="https://acme.ssl.com/sslcom-dv-rsa" +CA_SSLCOM_ECC="https://acme.ssl.com/sslcom-dv-ecc" + +DEFAULT_CA=$CA_ZEROSSL DEFAULT_STAGING_CA=$CA_LETSENCRYPT_V2_TEST CA_NAMES=" +ZeroSSL.com,zerossl LetsEncrypt.org,letsencrypt LetsEncrypt.org_test,letsencrypt_test,letsencrypttest BuyPass.com,buypass BuyPass.com_test,buypass_test,buypasstest -ZeroSSL.com,zerossl +SSL.com,sslcom " -CA_SERVERS="$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_ZEROSSL" +CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA" DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)" @@ -56,6 +59,9 @@ VTYPE_HTTP="http-01" VTYPE_DNS="dns-01" VTYPE_ALPN="tls-alpn-01" +ID_TYPE_DNS="dns" +ID_TYPE_IP="ip" + LOCAL_ANY_ADDRESS="0.0.0.0" DEFAULT_RENEW=60 @@ -102,6 +108,8 @@ DEBUG_LEVEL_NONE=0 DOH_CLOUDFLARE=1 DOH_GOOGLE=2 +DOH_ALI=3 +DOH_DP=4 HIDDEN_VALUE="[hidden](please add '--output-insecure' to see this value)" @@ -156,6 +164,8 @@ _REVOKE_WIKI="https://github.com/acmesh-official/acme.sh/wiki/revokecert" _ZEROSSL_WIKI="https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA" +_SSLCOM_WIKI="https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA" + _SERVER_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Server" _PREFERRED_CHAIN_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Preferred-Chain" @@ -419,19 +429,27 @@ _secure_debug3() { } _upper_case() { - # shellcheck disable=SC2018,SC2019 - tr 'a-z' 'A-Z' + if _is_solaris; then + tr '[:lower:]' '[:upper:]' + else + # shellcheck disable=SC2018,SC2019 + tr 'a-z' 'A-Z' + fi } _lower_case() { - # shellcheck disable=SC2018,SC2019 - tr 'A-Z' 'a-z' + if _is_solaris; then + tr '[:upper:]' '[:lower:]' + else + # shellcheck disable=SC2018,SC2019 + tr 'A-Z' 'a-z' + fi } _startswith() { _str="$1" _sub="$2" - echo "$_str" | grep "^$_sub" >/dev/null 2>&1 + echo "$_str" | grep -- "^$_sub" >/dev/null 2>&1 } _endswith() { @@ -562,8 +580,16 @@ if _exists xargs && [ "$(printf %s '\\x41' | xargs printf)" = 'A' ]; then fi _h2b() { - if _exists xxd && xxd -r -p 2>/dev/null; then - return + if _exists xxd; then + if _contains "$(xxd --help 2>&1)" "assumes -c30"; then + if xxd -r -p -c 9999 2>/dev/null; then + return + fi + else + if xxd -r -p 2>/dev/null; then + return + fi + fi fi hex=$(cat) @@ -1124,7 +1150,7 @@ _createkey() { if _isEccKey "$length"; then _debug "Using ec name: $eccname" - if _opkey="$(${ACME_OPENSSL_BIN:-openssl} ecparam -name "$eccname" -genkey 2>/dev/null)"; then + if _opkey="$(${ACME_OPENSSL_BIN:-openssl} ecparam -name "$eccname" -noout -genkey 2>/dev/null)"; then echo "$_opkey" >"$f" else _err "error ecc key name: $eccname" @@ -1132,7 +1158,11 @@ _createkey() { fi else _debug "Using RSA: $length" - if _opkey="$(${ACME_OPENSSL_BIN:-openssl} genrsa "$length" 2>/dev/null)"; then + __traditional="" + if _contains "$(${ACME_OPENSSL_BIN:-openssl} help genrsa 2>&1)" "-traditional"; then + __traditional="-traditional" + fi + if _opkey="$(${ACME_OPENSSL_BIN:-openssl} genrsa $__traditional "$length" 2>/dev/null)"; then echo "$_opkey" >"$f" else _err "error rsa key: $length" @@ -1199,23 +1229,31 @@ _createcsr() { _debug2 csr "$csr" _debug2 csrconf "$csrconf" - printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]\n\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment" >"$csrconf" + printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]\n\n" >"$csrconf" if [ "$acmeValidationv1" ]; then domainlist="$(_idn "$domainlist")" - printf -- "\nsubjectAltName=DNS:$domainlist" >>"$csrconf" + _debug2 domainlist "$domainlist" + alt="" + for dl in $(echo "$domainlist" | tr "," ' '); do + if [ "$alt" ]; then + alt="$alt,$(_getIdType "$dl" | _upper_case):$dl" + else + alt="$(_getIdType "$dl" | _upper_case):$dl" + fi + done + printf -- "\nsubjectAltName=$alt" >>"$csrconf" elif [ -z "$domainlist" ] || [ "$domainlist" = "$NO_VALUE" ]; then #single domain _info "Single domain" "$domain" - printf -- "\nsubjectAltName=DNS:$(_idn "$domain")" >>"$csrconf" + printf -- "\nsubjectAltName=$(_getIdType "$domain" | _upper_case):$(_idn "$domain")" >>"$csrconf" else domainlist="$(_idn "$domainlist")" _debug2 domainlist "$domainlist" - if _contains "$domainlist" ","; then - alt="DNS:$(_idn "$domain"),DNS:$(echo "$domainlist" | sed "s/,,/,/g" | sed "s/,/,DNS:/g")" - else - alt="DNS:$(_idn "$domain"),DNS:$domainlist" - fi + alt="$(_getIdType "$domain" | _upper_case):$domain" + for dl in $(echo "$domainlist" | tr "," ' '); do + alt="$alt,$(_getIdType "$dl" | _upper_case):$dl" + done #multi _info "Multi domain" "$alt" printf -- "\nsubjectAltName=$alt" >>"$csrconf" @@ -1751,7 +1789,7 @@ _inithttp() { if [ -z "$ACME_HTTP_NO_REDIRECTS" ]; then _ACME_CURL="$_ACME_CURL -L " fi - if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + if [ "$DEBUG" ] && [ "$DEBUG" -ge 2 ]; then _CURL_DUMP="$(_mktemp)" _ACME_CURL="$_ACME_CURL --trace-ascii $_CURL_DUMP " fi @@ -1791,6 +1829,8 @@ _inithttp() { } +_HTTP_MAX_RETRY=8 + # body url [needbase64] [POST|PUT|DELETE] [ContentType] _post() { body="$1" @@ -1798,6 +1838,33 @@ _post() { needbase64="$3" httpmethod="$4" _postContentType="$5" + _sleep_retry_sec=1 + _http_retry_times=0 + _hcode=0 + while [ "${_http_retry_times}" -le "$_HTTP_MAX_RETRY" ]; do + [ "$_http_retry_times" = "$_HTTP_MAX_RETRY" ] + _lastHCode="$?" + _debug "Retrying post" + _post_impl "$body" "$_post_url" "$needbase64" "$httpmethod" "$_postContentType" "$_lastHCode" + _hcode="$?" + _debug _hcode "$_hcode" + if [ "$_hcode" = "0" ]; then + break + fi + _http_retry_times=$(_math $_http_retry_times + 1) + _sleep $_sleep_retry_sec + done + return $_hcode +} + +# body url [needbase64] [POST|PUT|DELETE] [ContentType] [displayError] +_post_impl() { + body="$1" + _post_url="$2" + needbase64="$3" + httpmethod="$4" + _postContentType="$5" + displayError="$6" if [ -z "$httpmethod" ]; then httpmethod="POST" @@ -1849,7 +1916,9 @@ _post() { fi _ret="$?" if [ "$_ret" != "0" ]; then - _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret" + if [ -z "$displayError" ] || [ "$displayError" = "0" ]; then + _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret" + fi if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then _err "Here is the curl dump log:" _err "$(cat "$_CURL_DUMP")" @@ -1905,7 +1974,9 @@ _post() { _debug "wget returns 8, the server returns a 'Bad request' response, lets process the response later." fi if [ "$_ret" != "0" ]; then - _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret" + if [ -z "$displayError" ] || [ "$displayError" = "0" ]; then + _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret" + fi fi _sed_i "s/^ *//g" "$HTTP_HEADER" else @@ -1919,13 +1990,38 @@ _post() { # url getheader timeout _get() { + url="$1" + onlyheader="$2" + t="$3" + _sleep_retry_sec=1 + _http_retry_times=0 + _hcode=0 + while [ "${_http_retry_times}" -le "$_HTTP_MAX_RETRY" ]; do + [ "$_http_retry_times" = "$_HTTP_MAX_RETRY" ] + _lastHCode="$?" + _debug "Retrying GET" + _get_impl "$url" "$onlyheader" "$t" "$_lastHCode" + _hcode="$?" + _debug _hcode "$_hcode" + if [ "$_hcode" = "0" ]; then + break + fi + _http_retry_times=$(_math $_http_retry_times + 1) + _sleep $_sleep_retry_sec + done + return $_hcode +} + +# url getheader timeout displayError +_get_impl() { _debug GET url="$1" onlyheader="$2" t="$3" + displayError="$4" _debug url "$url" _debug "timeout=$t" - + _debug "displayError" "$displayError" _inithttp if [ "$_ACME_CURL" ] && [ "${ACME_USE_WGET:-0}" = "0" ]; then @@ -1944,7 +2040,9 @@ _get() { fi ret=$? if [ "$ret" != "0" ]; then - _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $ret" + if [ -z "$displayError" ] || [ "$displayError" = "0" ]; then + _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $ret" + fi if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then _err "Here is the curl dump log:" _err "$(cat "$_CURL_DUMP")" @@ -1970,7 +2068,9 @@ _get() { _debug "wget returns 8, the server returns a 'Bad request' response, lets process the response later." fi if [ "$ret" != "0" ]; then - _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $ret" + if [ -z "$displayError" ] || [ "$displayError" = "0" ]; then + _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $ret" + fi fi else ret=$? @@ -2026,7 +2126,7 @@ _send_signed_request() { if _post "" "$nonceurl" "" "HEAD" "$__request_conent_type" >/dev/null; then _headers="$(cat "$HTTP_HEADER")" _debug2 _headers "$_headers" - _CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + _CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2 | cut -d , -f 1)" fi fi if [ -z "$_CACHED_NONCE" ]; then @@ -2058,17 +2158,15 @@ _send_signed_request() { _sleep 2 continue fi - if [ "$ACME_VERSION" = "2" ]; then - if [ "$url" = "$ACME_NEW_ACCOUNT" ]; then - protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}' - elif [ "$url" = "$ACME_REVOKE_CERT" ] && [ "$keyfile" != "$ACCOUNT_KEY_PATH" ]; then - protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}' - else - protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"kid\": \"${ACCOUNT_URL}\""'}' - fi - else + + if [ "$url" = "$ACME_NEW_ACCOUNT" ]; then protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}' + elif [ "$url" = "$ACME_REVOKE_CERT" ] && [ "$keyfile" != "$ACCOUNT_KEY_PATH" ]; then + protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}' + else + protected="$JWK_HEADERPLACE_PART1$nonce\", \"url\": \"${url}$JWK_HEADERPLACE_PART2, \"kid\": \"${ACCOUNT_URL}\""'}' fi + _debug3 protected "$protected" protected64="$(printf "%s" "$protected" | _base64 | _url_replace)" @@ -2106,7 +2204,7 @@ _send_signed_request() { fi _debug2 response "$response" - _CACHED_NONCE="$(echo "$responseHeaders" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + _CACHED_NONCE="$(echo "$responseHeaders" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2 | cut -d , -f 1)" if ! _startswith "$code" "2"; then _body="$response" @@ -2121,6 +2219,12 @@ _send_signed_request() { _sleep $_sleep_retry_sec continue fi + if _contains "$_body" "The Replay Nonce is not recognized"; then + _info "The replay Nonce is not valid, let's get a new one, Sleeping $_sleep_retry_sec seconds." + _CACHED_NONCE="" + _sleep $_sleep_retry_sec + continue + fi fi return 0 done @@ -2248,7 +2352,7 @@ _getdeployconf() { return 0 # do nothing fi _saved=$(_readdomainconf "SAVED_$_rac_key") - eval "export $_rac_key=\"$_saved\"" + eval "export $_rac_key=\"\$_saved\"" } #_saveaccountconf key value base64encode @@ -2279,6 +2383,13 @@ _clearaccountconf() { _clear_conf "$ACCOUNT_CONF_PATH" "$1" } +#key +_clearaccountconf_mutable() { + _clearaccountconf "SAVED_$1" + #remove later + _clearaccountconf "$1" +} + #_savecaconf key value _savecaconf() { _save_conf "$CA_CONF" "$1" "$2" @@ -2332,7 +2443,7 @@ _startserver() { echo 'HTTP/1.0 200 OK'; \ echo 'Content-Length\: $_content_len'; \ echo ''; \ -printf -- '$content';" & +printf '%s' '$content';" & serverproc="$!" } @@ -2507,76 +2618,56 @@ __initHome() { fi } +_clearAPI() { + ACME_NEW_ACCOUNT="" + ACME_KEY_CHANGE="" + ACME_NEW_AUTHZ="" + ACME_NEW_ORDER="" + ACME_REVOKE_CERT="" + ACME_NEW_NONCE="" + ACME_AGREEMENT="" +} + #server _initAPI() { _api_server="${1:-$ACME_DIRECTORY}" _debug "_init api for server: $_api_server" - if [ -z "$ACME_NEW_ACCOUNT" ]; then + MAX_API_RETRY_TIMES=10 + _sleep_retry_sec=10 + _request_retry_times=0 + while [ -z "$ACME_NEW_ACCOUNT" ] && [ "${_request_retry_times}" -lt "$MAX_API_RETRY_TIMES" ]; do + _request_retry_times=$(_math "$_request_retry_times" + 1) response=$(_get "$_api_server") if [ "$?" != "0" ]; then _debug2 "response" "$response" - _err "Can not init api." - return 1 + _info "Can not init api for: $_api_server." + _info "Sleep $_sleep_retry_sec and retry." + _sleep "$_sleep_retry_sec" + continue fi response=$(echo "$response" | _json_decode) _debug2 "response" "$response" - ACME_KEY_CHANGE=$(echo "$response" | _egrep_o 'key-change" *: *"[^"]*"' | cut -d '"' -f 3) - if [ -z "$ACME_KEY_CHANGE" ]; then - ACME_KEY_CHANGE=$(echo "$response" | _egrep_o 'keyChange" *: *"[^"]*"' | cut -d '"' -f 3) - fi + ACME_KEY_CHANGE=$(echo "$response" | _egrep_o 'keyChange" *: *"[^"]*"' | cut -d '"' -f 3) export ACME_KEY_CHANGE - ACME_NEW_AUTHZ=$(echo "$response" | _egrep_o 'new-authz" *: *"[^"]*"' | cut -d '"' -f 3) - if [ -z "$ACME_NEW_AUTHZ" ]; then - ACME_NEW_AUTHZ=$(echo "$response" | _egrep_o 'newAuthz" *: *"[^"]*"' | cut -d '"' -f 3) - fi + ACME_NEW_AUTHZ=$(echo "$response" | _egrep_o 'newAuthz" *: *"[^"]*"' | cut -d '"' -f 3) export ACME_NEW_AUTHZ - ACME_NEW_ORDER=$(echo "$response" | _egrep_o 'new-cert" *: *"[^"]*"' | cut -d '"' -f 3) - ACME_NEW_ORDER_RES="new-cert" - if [ -z "$ACME_NEW_ORDER" ]; then - ACME_NEW_ORDER=$(echo "$response" | _egrep_o 'new-order" *: *"[^"]*"' | cut -d '"' -f 3) - ACME_NEW_ORDER_RES="new-order" - if [ -z "$ACME_NEW_ORDER" ]; then - ACME_NEW_ORDER=$(echo "$response" | _egrep_o 'newOrder" *: *"[^"]*"' | cut -d '"' -f 3) - fi - fi + ACME_NEW_ORDER=$(echo "$response" | _egrep_o 'newOrder" *: *"[^"]*"' | cut -d '"' -f 3) export ACME_NEW_ORDER - export ACME_NEW_ORDER_RES - ACME_NEW_ACCOUNT=$(echo "$response" | _egrep_o 'new-reg" *: *"[^"]*"' | cut -d '"' -f 3) - ACME_NEW_ACCOUNT_RES="new-reg" - if [ -z "$ACME_NEW_ACCOUNT" ]; then - ACME_NEW_ACCOUNT=$(echo "$response" | _egrep_o 'new-account" *: *"[^"]*"' | cut -d '"' -f 3) - ACME_NEW_ACCOUNT_RES="new-account" - if [ -z "$ACME_NEW_ACCOUNT" ]; then - ACME_NEW_ACCOUNT=$(echo "$response" | _egrep_o 'newAccount" *: *"[^"]*"' | cut -d '"' -f 3) - if [ "$ACME_NEW_ACCOUNT" ]; then - export ACME_VERSION=2 - fi - fi - fi + ACME_NEW_ACCOUNT=$(echo "$response" | _egrep_o 'newAccount" *: *"[^"]*"' | cut -d '"' -f 3) export ACME_NEW_ACCOUNT - export ACME_NEW_ACCOUNT_RES - ACME_REVOKE_CERT=$(echo "$response" | _egrep_o 'revoke-cert" *: *"[^"]*"' | cut -d '"' -f 3) - if [ -z "$ACME_REVOKE_CERT" ]; then - ACME_REVOKE_CERT=$(echo "$response" | _egrep_o 'revokeCert" *: *"[^"]*"' | cut -d '"' -f 3) - fi + ACME_REVOKE_CERT=$(echo "$response" | _egrep_o 'revokeCert" *: *"[^"]*"' | cut -d '"' -f 3) export ACME_REVOKE_CERT - ACME_NEW_NONCE=$(echo "$response" | _egrep_o 'new-nonce" *: *"[^"]*"' | cut -d '"' -f 3) - if [ -z "$ACME_NEW_NONCE" ]; then - ACME_NEW_NONCE=$(echo "$response" | _egrep_o 'newNonce" *: *"[^"]*"' | cut -d '"' -f 3) - fi + ACME_NEW_NONCE=$(echo "$response" | _egrep_o 'newNonce" *: *"[^"]*"' | cut -d '"' -f 3) export ACME_NEW_NONCE - ACME_AGREEMENT=$(echo "$response" | _egrep_o 'terms-of-service" *: *"[^"]*"' | cut -d '"' -f 3) - if [ -z "$ACME_AGREEMENT" ]; then - ACME_AGREEMENT=$(echo "$response" | _egrep_o 'termsOfService" *: *"[^"]*"' | cut -d '"' -f 3) - fi + ACME_AGREEMENT=$(echo "$response" | _egrep_o 'termsOfService" *: *"[^"]*"' | cut -d '"' -f 3) export ACME_AGREEMENT _debug "ACME_KEY_CHANGE" "$ACME_KEY_CHANGE" @@ -2586,9 +2677,17 @@ _initAPI() { _debug "ACME_REVOKE_CERT" "$ACME_REVOKE_CERT" _debug "ACME_AGREEMENT" "$ACME_AGREEMENT" _debug "ACME_NEW_NONCE" "$ACME_NEW_NONCE" - _debug "ACME_VERSION" "$ACME_VERSION" - + if [ "$ACME_NEW_ACCOUNT" ] && [ "$ACME_NEW_ORDER" ]; then + return 0 + fi + _info "Sleep $_sleep_retry_sec and retry." + _sleep "$_sleep_retry_sec" + done + if [ "$ACME_NEW_ACCOUNT" ] && [ "$ACME_NEW_ORDER" ]; then + return 0 fi + _err "Can not init api, for $_api_server" + return 1 } #[domain] [keylength or isEcc flag] @@ -2632,15 +2731,44 @@ _initpath() { _ACME_SERVER_HOST="$(echo "$ACME_DIRECTORY" | cut -d : -f 2 | tr -s / | cut -d / -f 2)" _debug2 "_ACME_SERVER_HOST" "$_ACME_SERVER_HOST" - CA_DIR="$CA_HOME/$_ACME_SERVER_HOST" + _ACME_SERVER_PATH="$(echo "$ACME_DIRECTORY" | cut -d : -f 2- | tr -s / | cut -d / -f 3-)" + _debug2 "_ACME_SERVER_PATH" "$_ACME_SERVER_PATH" + CA_DIR="$CA_HOME/$_ACME_SERVER_HOST/$_ACME_SERVER_PATH" _DEFAULT_CA_CONF="$CA_DIR/ca.conf" - if [ -z "$CA_CONF" ]; then CA_CONF="$_DEFAULT_CA_CONF" fi _debug3 CA_CONF "$CA_CONF" + _OLD_CADIR="$CA_HOME/$_ACME_SERVER_HOST" + _OLD_ACCOUNT_KEY="$_OLD_CADIR/account.key" + _OLD_ACCOUNT_JSON="$_OLD_CADIR/account.json" + _OLD_CA_CONF="$_OLD_CADIR/ca.conf" + + _DEFAULT_ACCOUNT_KEY_PATH="$CA_DIR/account.key" + _DEFAULT_ACCOUNT_JSON_PATH="$CA_DIR/account.json" + if [ -z "$ACCOUNT_KEY_PATH" ]; then + ACCOUNT_KEY_PATH="$_DEFAULT_ACCOUNT_KEY_PATH" + if [ -f "$_OLD_ACCOUNT_KEY" ] && ! [ -f "$ACCOUNT_KEY_PATH" ]; then + mkdir -p "$CA_DIR" + mv "$_OLD_ACCOUNT_KEY" "$ACCOUNT_KEY_PATH" + fi + fi + + if [ -z "$ACCOUNT_JSON_PATH" ]; then + ACCOUNT_JSON_PATH="$_DEFAULT_ACCOUNT_JSON_PATH" + if [ -f "$_OLD_ACCOUNT_JSON" ] && ! [ -f "$ACCOUNT_JSON_PATH" ]; then + mkdir -p "$CA_DIR" + mv "$_OLD_ACCOUNT_JSON" "$ACCOUNT_JSON_PATH" + fi + fi + + if [ -f "$_OLD_CA_CONF" ] && ! [ -f "$CA_CONF" ]; then + mkdir -p "$CA_DIR" + mv "$_OLD_CA_CONF" "$CA_CONF" + fi + if [ -f "$CA_CONF" ]; then . "$CA_CONF" fi @@ -2661,19 +2789,6 @@ _initpath() { HTTP_HEADER="$LE_CONFIG_HOME/http.header" fi - _OLD_ACCOUNT_KEY="$LE_WORKING_DIR/account.key" - _OLD_ACCOUNT_JSON="$LE_WORKING_DIR/account.json" - - _DEFAULT_ACCOUNT_KEY_PATH="$CA_DIR/account.key" - _DEFAULT_ACCOUNT_JSON_PATH="$CA_DIR/account.json" - if [ -z "$ACCOUNT_KEY_PATH" ]; then - ACCOUNT_KEY_PATH="$_DEFAULT_ACCOUNT_KEY_PATH" - fi - - if [ -z "$ACCOUNT_JSON_PATH" ]; then - ACCOUNT_JSON_PATH="$_DEFAULT_ACCOUNT_JSON_PATH" - fi - _DEFAULT_CERT_HOME="$LE_CONFIG_HOME" if [ -z "$CERT_HOME" ]; then CERT_HOME="$_DEFAULT_CERT_HOME" @@ -3071,6 +3186,11 @@ _checkConf() { _debug "Try include files" for included in $(cat "$2" | tr "\t" " " | grep "^ *include *.*;" | sed "s/include //" | tr -d " ;"); do _debug "check included $included" + if ! _startswith "$included" "/" && _exists dirname; then + _relpath="$(dirname "$2")" + _debug "_relpath" "$_relpath" + included="$_relpath/$included" + fi if _checkConf "$1" "$included"; then return 0 fi @@ -3281,6 +3401,8 @@ _on_before_issue() { if [ "$_chk_pre_hook" ]; then _info "Run pre hook:'$_chk_pre_hook'" if ! ( + export Le_Domain="$_chk_main_domain" + export Le_Alt="$_chk_alt_domains" cd "$DOMAIN_PATH" && eval "$_chk_pre_hook" ); then _err "Error when run pre hook." @@ -3342,7 +3464,7 @@ _on_before_issue() { _netprc="$(_ss "$_checkport" | grep "$_checkport")" netprc="$(echo "$_netprc" | grep "$_checkaddr")" if [ -z "$netprc" ]; then - netprc="$(echo "$_netprc" | grep "$LOCAL_ANY_ADDRESS")" + netprc="$(echo "$_netprc" | grep "$LOCAL_ANY_ADDRESS:$_checkport")" fi if [ "$netprc" ]; then _err "$netprc" @@ -3499,15 +3621,6 @@ _regAccount() { _initAPI mkdir -p "$CA_DIR" - if [ ! -f "$ACCOUNT_KEY_PATH" ] && [ -f "$_OLD_ACCOUNT_KEY" ]; then - _info "mv $_OLD_ACCOUNT_KEY to $ACCOUNT_KEY_PATH" - mv "$_OLD_ACCOUNT_KEY" "$ACCOUNT_KEY_PATH" - fi - - if [ ! -f "$ACCOUNT_JSON_PATH" ] && [ -f "$_OLD_ACCOUNT_JSON" ]; then - _info "mv $_OLD_ACCOUNT_JSON to $ACCOUNT_JSON_PATH" - mv "$_OLD_ACCOUNT_JSON" "$ACCOUNT_JSON_PATH" - fi if [ ! -f "$ACCOUNT_KEY_PATH" ]; then if ! _create_account_key "$_reg_length"; then @@ -3531,68 +3644,66 @@ _regAccount() { if [ "$_email" ]; then _savecaconf "CA_EMAIL" "$_email" fi - if [ "$ACME_VERSION" = "2" ]; then - if [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then - if [ -z "$_eab_id" ] || [ -z "$_eab_hmac_key" ]; then - _info "No EAB credentials found for ZeroSSL, let's get one" - if [ -z "$_email" ]; then - _err "Please provide a email address for ZeroSSL account." - _err "See ZeroSSL usage: $_ZEROSSL_WIKI" - return 1 - fi - _eabresp=$(_post "email=$_email" $_ZERO_EAB_ENDPOINT) - if [ "$?" != "0" ]; then - _debug2 "$_eabresp" - _err "Can not get EAB credentials from ZeroSSL." - return 1 - fi - _eab_id="$(echo "$_eabresp" | tr ',}' '\n' | grep '"eab_kid"' | cut -d : -f 2 | tr -d '"')" - if [ -z "$_eab_id" ]; then - _err "Can not resolve _eab_id" - return 1 - fi - _eab_hmac_key="$(echo "$_eabresp" | tr ',}' '\n' | grep '"eab_hmac_key"' | cut -d : -f 2 | tr -d '"')" - if [ -z "$_eab_hmac_key" ]; then - _err "Can not resolve _eab_hmac_key" - return 1 - fi - _savecaconf CA_EAB_KEY_ID "$_eab_id" - _savecaconf CA_EAB_HMAC_KEY "$_eab_hmac_key" + + if [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then + if [ -z "$_eab_id" ] || [ -z "$_eab_hmac_key" ]; then + _info "No EAB credentials found for ZeroSSL, let's get one" + if [ -z "$_email" ]; then + _info "$(__green "$PROJECT_NAME is using ZeroSSL as default CA now.")" + _info "$(__green "Please update your account with an email address first.")" + _info "$(__green "$PROJECT_ENTRY --register-account -m my@example.com")" + _info "See: $(__green "$_ZEROSSL_WIKI")" + return 1 fi - fi - 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) - _debug3 eab_protected64 "$eab_protected64" - - eab_payload64=$(printf "%s" "$jwk" | _base64 | _url_replace) - _debug3 eab_payload64 "$eab_payload64" - - eab_sign_t="$eab_protected64.$eab_payload64" - _debug3 eab_sign_t "$eab_sign_t" - - key_hex="$(_durl_replace_base64 "$_eab_hmac_key" | _dbase64 | _hex_dump | tr -d ' ')" - _debug3 key_hex "$key_hex" - - eab_signature=$(printf "%s" "$eab_sign_t" | _hmac sha256 $key_hex | _base64 | _url_replace) - _debug3 eab_signature "$eab_signature" - - externalBinding=",\"externalAccountBinding\":{\"protected\":\"$eab_protected64\", \"payload\":\"$eab_payload64\", \"signature\":\"$eab_signature\"}" - _debug3 externalBinding "$externalBinding" - fi - if [ "$_email" ]; then - email_sg="\"contact\": [\"mailto:$_email\"], " - fi - regjson="{$email_sg\"termsOfServiceAgreed\": true$externalBinding}" - else - _reg_res="$ACME_NEW_ACCOUNT_RES" - regjson='{"resource": "'$_reg_res'", "terms-of-service-agreed": true, "agreement": "'$ACME_AGREEMENT'"}' - if [ "$_email" ]; then - regjson='{"resource": "'$_reg_res'", "contact": ["mailto:'$_email'"], "terms-of-service-agreed": true, "agreement": "'$ACME_AGREEMENT'"}' + _eabresp=$(_post "email=$_email" $_ZERO_EAB_ENDPOINT) + if [ "$?" != "0" ]; then + _debug2 "$_eabresp" + _err "Can not get EAB credentials from ZeroSSL." + return 1 + fi + _secure_debug2 _eabresp "$_eabresp" + _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 "Can not resolve _eab_id" + return 1 + fi + _eab_hmac_key="$(echo "$_eabresp" | tr ',}' '\n\n' | grep '"eab_hmac_key"' | cut -d : -f 2 | tr -d '"')" + _secure_debug2 _eab_hmac_key "$_eab_hmac_key" + if [ -z "$_eab_hmac_key" ]; then + _err "Can not resolve _eab_hmac_key" + return 1 + fi + _savecaconf CA_EAB_KEY_ID "$_eab_id" + _savecaconf CA_EAB_HMAC_KEY "$_eab_hmac_key" fi fi + 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) + _debug3 eab_protected64 "$eab_protected64" + + eab_payload64=$(printf "%s" "$jwk" | _base64 | _url_replace) + _debug3 eab_payload64 "$eab_payload64" + + eab_sign_t="$eab_protected64.$eab_payload64" + _debug3 eab_sign_t "$eab_sign_t" + + key_hex="$(_durl_replace_base64 "$_eab_hmac_key" | _dbase64 multi | _hex_dump | tr -d ' ')" + _debug3 key_hex "$key_hex" + + eab_signature=$(printf "%s" "$eab_sign_t" | _hmac sha256 $key_hex | _base64 | _url_replace) + _debug3 eab_signature "$eab_signature" + + externalBinding=",\"externalAccountBinding\":{\"protected\":\"$eab_protected64\", \"payload\":\"$eab_payload64\", \"signature\":\"$eab_signature\"}" + _debug3 externalBinding "$externalBinding" + fi + if [ "$_email" ]; then + email_sg="\"contact\": [\"mailto:$_email\"], " + fi + regjson="{$email_sg\"termsOfServiceAgreed\": true$externalBinding}" _info "Registering account: $ACME_DIRECTORY" @@ -3647,16 +3758,6 @@ _regAccount() { updateaccount() { _initpath - if [ ! -f "$ACCOUNT_KEY_PATH" ] && [ -f "$_OLD_ACCOUNT_KEY" ]; then - _info "mv $_OLD_ACCOUNT_KEY to $ACCOUNT_KEY_PATH" - mv "$_OLD_ACCOUNT_KEY" "$ACCOUNT_KEY_PATH" - fi - - if [ ! -f "$ACCOUNT_JSON_PATH" ] && [ -f "$_OLD_ACCOUNT_JSON" ]; then - _info "mv $_OLD_ACCOUNT_JSON to $ACCOUNT_JSON_PATH" - mv "$_OLD_ACCOUNT_JSON" "$ACCOUNT_JSON_PATH" - fi - if [ ! -f "$ACCOUNT_KEY_PATH" ]; then _err "Account key is not found at: $ACCOUNT_KEY_PATH" return 1 @@ -3677,20 +3778,13 @@ updateaccount() { _initAPI _email="$(_getAccountEmail)" - if [ "$ACME_VERSION" = "2" ]; then - if [ "$ACCOUNT_EMAIL" ]; then - updjson='{"contact": ["mailto:'$_email'"]}' - else - updjson='{"contact": []}' - fi + + if [ "$ACCOUNT_EMAIL" ]; then + updjson='{"contact": ["mailto:'$_email'"]}' else - # ACMEv1: Updates happen the same way a registration is done. - # https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-6.3 - _regAccount - return + updjson='{"contact": []}' fi - # this part handles ACMEv2 account updates. _send_signed_request "$_accUri" "$updjson" if [ "$code" = '200' ]; then @@ -3706,16 +3800,6 @@ updateaccount() { deactivateaccount() { _initpath - if [ ! -f "$ACCOUNT_KEY_PATH" ] && [ -f "$_OLD_ACCOUNT_KEY" ]; then - _info "mv $_OLD_ACCOUNT_KEY to $ACCOUNT_KEY_PATH" - mv "$_OLD_ACCOUNT_KEY" "$ACCOUNT_KEY_PATH" - fi - - if [ ! -f "$ACCOUNT_JSON_PATH" ] && [ -f "$_OLD_ACCOUNT_JSON" ]; then - _info "mv $_OLD_ACCOUNT_JSON to $ACCOUNT_JSON_PATH" - mv "$_OLD_ACCOUNT_JSON" "$ACCOUNT_JSON_PATH" - fi - if [ ! -f "$ACCOUNT_KEY_PATH" ]; then _err "Account key is not found at: $ACCOUNT_KEY_PATH" return 1 @@ -3735,11 +3819,8 @@ deactivateaccount() { fi _initAPI - if [ "$ACME_VERSION" = "2" ]; then - _djson="{\"status\":\"deactivated\"}" - else - _djson="{\"resource\": \"reg\", \"status\":\"deactivated\"}" - fi + _djson="{\"status\":\"deactivated\"}" + if _send_signed_request "$_accUri" "$_djson" && _contains "$response" '"deactivated"'; then _info "Deactivate account success for $_accUri." _accid=$(echo "$response" | _egrep_o "\"id\" *: *[^,]*," | cut -d : -f 2 | tr -d ' ,') @@ -3844,11 +3925,9 @@ __trigger_validation() { _debug2 _t_key_authz "$_t_key_authz" _t_vtype="$3" _debug2 _t_vtype "$_t_vtype" - if [ "$ACME_VERSION" = "2" ]; then - _send_signed_request "$_t_url" "{}" - else - _send_signed_request "$_t_url" "{\"resource\": \"challenge\", \"type\": \"$_t_vtype\", \"keyAuthorization\": \"$_t_key_authz\"}" - fi + + _send_signed_request "$_t_url" "{}" + } #endpoint domain type @@ -3891,7 +3970,15 @@ _ns_purge_cf() { #checks if cf server is available _ns_is_available_cf() { - if _get "https://cloudflare-dns.com" >/dev/null 2>&1; then + if _get "https://cloudflare-dns.com" "" 1 >/dev/null 2>&1; then + return 0 + else + return 1 + fi +} + +_ns_is_available_google() { + if _get "https://dns.google" "" 1 >/dev/null 2>&1; then return 0 else return 1 @@ -3906,6 +3993,38 @@ _ns_lookup_google() { _ns_lookup_impl "$_cf_ep" "$_cf_ld" "$_cf_ld_type" } +_ns_is_available_ali() { + if _get "https://dns.alidns.com" "" 1 >/dev/null 2>&1; then + return 0 + else + return 1 + fi +} + +#domain, type +_ns_lookup_ali() { + _cf_ld="$1" + _cf_ld_type="$2" + _cf_ep="https://dns.alidns.com/resolve" + _ns_lookup_impl "$_cf_ep" "$_cf_ld" "$_cf_ld_type" +} + +_ns_is_available_dp() { + if _get "https://doh.pub" "" 1 >/dev/null 2>&1; then + return 0 + else + return 1 + fi +} + +#dnspod +_ns_lookup_dp() { + _cf_ld="$1" + _cf_ld_type="$2" + _cf_ep="https://doh.pub/dns-query" + _ns_lookup_impl "$_cf_ep" "$_cf_ld" "$_cf_ld_type" +} + #domain, type _ns_lookup() { if [ -z "$DOH_USE" ]; then @@ -3913,16 +4032,30 @@ _ns_lookup() { if _ns_is_available_cf; then _debug "Use cloudflare doh server" export DOH_USE=$DOH_CLOUDFLARE - else + elif _ns_is_available_google; then _debug "Use google doh server" export DOH_USE=$DOH_GOOGLE + elif _ns_is_available_ali; then + _debug "Use aliyun doh server" + export DOH_USE=$DOH_ALI + elif _ns_is_available_dp; then + _debug "Use dns pod doh server" + export DOH_USE=$DOH_DP + else + _err "No doh" fi fi if [ "$DOH_USE" = "$DOH_CLOUDFLARE" ] || [ -z "$DOH_USE" ]; then _ns_lookup_cf "$@" - else + elif [ "$DOH_USE" = "$DOH_GOOGLE" ]; then _ns_lookup_google "$@" + elif [ "$DOH_USE" = "$DOH_ALI" ]; then + _ns_lookup_ali "$@" + elif [ "$DOH_USE" = "$DOH_DP" ]; then + _ns_lookup_dp "$@" + else + _err "Unknown doh provider: DOH_USE=$DOH_USE" fi } @@ -3947,7 +4080,7 @@ __purge_txt() { if [ "$DOH_USE" = "$DOH_CLOUDFLARE" ] || [ -z "$DOH_USE" ]; then _ns_purge_cf "$_p_txtdomain" "TXT" else - _debug "no purge api for google dns api, just sleep 5 secs" + _debug "no purge api for this doh api, just sleep 5 secs" _sleep 5 fi @@ -4009,12 +4142,42 @@ _check_dns_entries() { } #file -_get_cert_issuers() { +_get_chain_issuers() { _cfile="$1" - if _contains "$(${ACME_OPENSSL_BIN:-openssl} help crl2pkcs7 2>&1)" "Usage: crl2pkcs7" || _contains "$(${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 help 2>&1)" "unknown option help"; then - ${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 -nocrl -certfile $_cfile | ${ACME_OPENSSL_BIN:-openssl} pkcs7 -print_certs -text -noout | grep 'Issuer:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2 + if _contains "$(${ACME_OPENSSL_BIN:-openssl} help crl2pkcs7 2>&1)" "Usage: crl2pkcs7" || _contains "$(${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 -help 2>&1)" "Usage: crl2pkcs7" || _contains "$(${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 help 2>&1)" "unknown option help"; then + ${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 -nocrl -certfile $_cfile | ${ACME_OPENSSL_BIN:-openssl} pkcs7 -print_certs -text -noout | grep -i 'Issuer:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2 else - ${ACME_OPENSSL_BIN:-openssl} x509 -in $_cfile -text -noout | grep 'Issuer:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2 + _cindex=1 + for _startn in $(grep -n -- "$BEGIN_CERT" "$_cfile" | cut -d : -f 1); do + _endn="$(grep -n -- "$END_CERT" "$_cfile" | cut -d : -f 1 | _head_n $_cindex | _tail_n 1)" + _debug2 "_startn" "$_startn" + _debug2 "_endn" "$_endn" + if [ "$DEBUG" ]; then + _debug2 "cert$_cindex" "$(sed -n "$_startn,${_endn}p" "$_cfile")" + fi + sed -n "$_startn,${_endn}p" "$_cfile" | ${ACME_OPENSSL_BIN:-openssl} x509 -text -noout | grep 'Issuer:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2 | sed "s/ *\(.*\)/\1/" + _cindex=$(_math $_cindex + 1) + done + fi +} + +# +_get_chain_subjects() { + _cfile="$1" + if _contains "$(${ACME_OPENSSL_BIN:-openssl} help crl2pkcs7 2>&1)" "Usage: crl2pkcs7" || _contains "$(${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 -help 2>&1)" "Usage: crl2pkcs7" || _contains "$(${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 help 2>&1)" "unknown option help"; then + ${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 -nocrl -certfile $_cfile | ${ACME_OPENSSL_BIN:-openssl} pkcs7 -print_certs -text -noout | grep -i 'Subject:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2 + else + _cindex=1 + for _startn in $(grep -n -- "$BEGIN_CERT" "$_cfile" | cut -d : -f 1); do + _endn="$(grep -n -- "$END_CERT" "$_cfile" | cut -d : -f 1 | _head_n $_cindex | _tail_n 1)" + _debug2 "_startn" "$_startn" + _debug2 "_endn" "$_endn" + if [ "$DEBUG" ]; then + _debug2 "cert$_cindex" "$(sed -n "$_startn,${_endn}p" "$_cfile")" + fi + sed -n "$_startn,${_endn}p" "$_cfile" | ${ACME_OPENSSL_BIN:-openssl} x509 -text -noout | grep -i 'Subject:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2 | sed "s/ *\(.*\)/\1/" + _cindex=$(_math $_cindex + 1) + done fi } @@ -4022,14 +4185,42 @@ _get_cert_issuers() { _match_issuer() { _cfile="$1" _missuer="$2" - _fissuers="$(_get_cert_issuers $_cfile)" + _fissuers="$(_get_chain_issuers $_cfile)" _debug2 _fissuers "$_fissuers" - if _contains "$_fissuers" "$_missuer"; then - return 0 - fi - _fissuers="$(echo "$_fissuers" | _lower_case)" + _rootissuer="$(echo "$_fissuers" | _lower_case | _tail_n 1)" + _debug2 _rootissuer "$_rootissuer" _missuer="$(echo "$_missuer" | _lower_case)" - _contains "$_fissuers" "$_missuer" + _contains "$_rootissuer" "$_missuer" +} + +#ip +_isIPv4() { + for seg in $(echo "$1" | tr '.' ' '); do + if [ $seg -ge 0 ] 2>/dev/null && [ $seg -le 255 ] 2>/dev/null; then + continue + fi + return 1 + done + return 0 +} + +#ip6 +_isIPv6() { + _contains "$1" ":" +} + +#ip +_isIP() { + _isIPv4 "$1" || _isIPv6 "$1" +} + +#identifier +_getIdType() { + if _isIP "$1"; then + echo "$ID_TYPE_IP" + else + echo "$ID_TYPE_DNS" + fi } #webroot, domain domainlist keylength @@ -4069,6 +4260,10 @@ issue() { if [ -z "$_ACME_IS_RENEW" ]; then _initpath "$_main_domain" "$_key_length" mkdir -p "$DOMAIN_PATH" + elif ! _hasfield "$_web_roots" "$W_DNS"; then + Le_OrderFinalize="" + Le_LinkOrder="" + Le_LinkCert="" fi if _hasfield "$_web_roots" "$W_DNS" && [ -z "$FORCE_DNS_MANUAL" ]; then @@ -4076,10 +4271,6 @@ issue() { return 1 fi - _debug "Using ACME_DIRECTORY: $ACME_DIRECTORY" - - _initAPI - if [ -f "$DOMAIN_CONF" ]; then Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime) _debug Le_NextRenewTime "$Le_NextRenewTime" @@ -4099,6 +4290,11 @@ issue() { fi fi + _debug "Using ACME_DIRECTORY: $ACME_DIRECTORY" + if ! _initAPI; then + return 1 + fi + _savedomainconf "Le_Domain" "$_main_domain" _savedomainconf "Le_Alt" "$_alt_domains" _savedomainconf "Le_Webroot" "$_web_roots" @@ -4182,74 +4378,72 @@ issue() { sep='#' dvsep=',' if [ -z "$vlist" ]; then - if [ "$ACME_VERSION" = "2" ]; then - #make new order request - _identifiers="{\"type\":\"dns\",\"value\":\"$(_idn "$_main_domain")\"}" - _w_index=1 - while true; do - d="$(echo "$_alt_domains," | cut -d , -f "$_w_index")" - _w_index="$(_math "$_w_index" + 1)" - _debug d "$d" - if [ -z "$d" ]; then - break - fi - _identifiers="$_identifiers,{\"type\":\"dns\",\"value\":\"$(_idn "$d")\"}" - done - _debug2 _identifiers "$_identifiers" - if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then - _err "Create new order error." - _clearup - _on_issue_err "$_post_hook" - return 1 + #make new order request + _identifiers="{\"type\":\"$(_getIdType "$_main_domain")\",\"value\":\"$(_idn "$_main_domain")\"}" + _w_index=1 + while true; do + d="$(echo "$_alt_domains," | cut -d , -f "$_w_index")" + _w_index="$(_math "$_w_index" + 1)" + _debug d "$d" + if [ -z "$d" ]; then + break fi - Le_LinkOrder="$(echo "$responseHeaders" | grep -i '^Location.*$' | _tail_n 1 | tr -d "\r\n " | cut -d ":" -f 2-)" - _debug Le_LinkOrder "$Le_LinkOrder" - Le_OrderFinalize="$(echo "$response" | _egrep_o '"finalize" *: *"[^"]*"' | cut -d '"' -f 4)" - _debug Le_OrderFinalize "$Le_OrderFinalize" - if [ -z "$Le_OrderFinalize" ]; then - _err "Create new order error. Le_OrderFinalize not found. $response" - _clearup - _on_issue_err "$_post_hook" - return 1 - fi - - #for dns manual mode - _savedomainconf "Le_OrderFinalize" "$Le_OrderFinalize" - - _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." - _clearup - _on_issue_err "$_post_hook" - return 1 - fi - - #domain and authz map - _authorizations_map="" - for _authz_url in $(echo "$_authorizations_seg" | tr ',' ' '); do - _debug2 "_authz_url" "$_authz_url" - if ! _send_signed_request "$_authz_url"; then - _err "get to authz error." - _err "_authorizations_seg" "$_authorizations_seg" - _err "_authz_url" "$_authz_url" - _clearup - _on_issue_err "$_post_hook" - return 1 - fi - - response="$(echo "$response" | _normalizeJson)" - _debug2 response "$response" - _d="$(echo "$response" | _egrep_o '"value" *: *"[^"]*"' | cut -d : -f 2 | tr -d ' "')" - if _contains "$response" "\"wildcard\" *: *true"; then - _d="*.$_d" - fi - _debug2 _d "$_d" - _authorizations_map="$_d,$response -$_authorizations_map" - done - _debug2 _authorizations_map "$_authorizations_map" + _identifiers="$_identifiers,{\"type\":\"$(_getIdType "$d")\",\"value\":\"$(_idn "$d")\"}" + done + _debug2 _identifiers "$_identifiers" + if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then + _err "Create new order error." + _clearup + _on_issue_err "$_post_hook" + return 1 fi + Le_LinkOrder="$(echo "$responseHeaders" | grep -i '^Location.*$' | _tail_n 1 | tr -d "\r\n " | cut -d ":" -f 2-)" + _debug Le_LinkOrder "$Le_LinkOrder" + Le_OrderFinalize="$(echo "$response" | _egrep_o '"finalize" *: *"[^"]*"' | cut -d '"' -f 4)" + _debug Le_OrderFinalize "$Le_OrderFinalize" + if [ -z "$Le_OrderFinalize" ]; then + _err "Create new order error. Le_OrderFinalize not found. $response" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + #for dns manual mode + _savedomainconf "Le_OrderFinalize" "$Le_OrderFinalize" + + _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." + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + #domain and authz map + _authorizations_map="" + for _authz_url in $(echo "$_authorizations_seg" | tr ',' ' '); do + _debug2 "_authz_url" "$_authz_url" + if ! _send_signed_request "$_authz_url"; then + _err "get to authz error." + _err "_authorizations_seg" "$_authorizations_seg" + _err "_authz_url" "$_authz_url" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + response="$(echo "$response" | _normalizeJson)" + _debug2 response "$response" + _d="$(echo "$response" | _egrep_o '"value" *: *"[^"]*"' | cut -d : -f 2 | tr -d ' "')" + if _contains "$response" "\"wildcard\" *: *true"; then + _d="*.$_d" + fi + _debug2 _d "$_d" + _authorizations_map="$_d,$response +$_authorizations_map" + done + _debug2 _authorizations_map "$_authorizations_map" _index=0 _currentRoot="" @@ -4280,33 +4474,25 @@ $_authorizations_map" vtype="$VTYPE_ALPN" fi - if [ "$ACME_VERSION" = "2" ]; then - _idn_d="$(_idn "$d")" - _candidates="$(echo "$_authorizations_map" | grep -i "^$_idn_d,")" - _debug2 _candidates "$_candidates" - if [ "$(echo "$_candidates" | wc -l)" -gt 1 ]; then - for _can in $_candidates; do - if _startswith "$(echo "$_can" | tr '.' '|')" "$(echo "$_idn_d" | tr '.' '|'),"; then - _candidates="$_can" - break - fi - done - fi - response="$(echo "$_candidates" | sed "s/$_idn_d,//")" - _debug2 "response" "$response" - if [ -z "$response" ]; then - _err "get to authz error." - _err "_authorizations_map" "$_authorizations_map" - _clearup - _on_issue_err "$_post_hook" - return 1 - fi - else - if ! __get_domain_new_authz "$d"; then - _clearup - _on_issue_err "$_post_hook" - return 1 - fi + _idn_d="$(_idn "$d")" + _candidates="$(echo "$_authorizations_map" | grep -i "^$_idn_d,")" + _debug2 _candidates "$_candidates" + if [ "$(echo "$_candidates" | wc -l)" -gt 1 ]; then + for _can in $_candidates; do + if _startswith "$(echo "$_can" | tr '.' '|')" "$(echo "$_idn_d" | tr '.' '|'),"; then + _candidates="$_can" + break + fi + done + fi + response="$(echo "$_candidates" | sed "s/$_idn_d,//")" + _debug2 "response" "$response" + if [ -z "$response" ]; then + _err "get to authz error." + _err "_authorizations_map" "$_authorizations_map" + _clearup + _on_issue_err "$_post_hook" + return 1 fi if [ -z "$thumbprint" ]; then @@ -4347,11 +4533,9 @@ $_authorizations_map" _on_issue_err "$_post_hook" return 1 fi - if [ "$ACME_VERSION" = "2" ]; then - uri="$(echo "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)" - else - uri="$(echo "$entry" | _egrep_o '"uri":"[^"]*' | cut -d '"' -f 4)" - fi + + uri="$(echo "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)" + _debug uri "$uri" if [ -z "$uri" ]; then @@ -4646,36 +4830,14 @@ $_authorizations_map" return 1 fi - _debug "sleep 2 secs to verify" - sleep 2 - _debug "checking" - if [ "$ACME_VERSION" = "2" ]; then - _send_signed_request "$uri" - else - response="$(_get "$uri")" - fi - if [ "$?" != "0" ]; then - _err "$d:Verify error:$response" - _clearupwebbroot "$_currentRoot" "$removelevel" "$token" - _clearup - _on_issue_err "$_post_hook" "$vlist" - return 1 - fi _debug2 original "$response" response="$(echo "$response" | _normalizeJson)" _debug2 response "$response" status=$(echo "$response" | _egrep_o '"status":"[^"]*' | cut -d : -f 2 | tr -d '"') - if [ "$status" = "valid" ]; then - _info "$(__green Success)" - _stopserver "$serverproc" - serverproc="" - _clearupwebbroot "$_currentRoot" "$removelevel" "$token" - break - fi - - if [ "$status" = "invalid" ]; then + _debug2 status "$status" + if _contains "$status" "invalid"; then error="$(echo "$response" | _egrep_o '"error":\{[^\}]*')" _debug2 error "$error" errordetail="$(echo "$error" | _egrep_o '"detail": *"[^"]*' | cut -d '"' -f 4)" @@ -4697,10 +4859,18 @@ $_authorizations_map" return 1 fi + if _contains "$status" "valid"; then + _info "$(__green Success)" + _stopserver "$serverproc" + serverproc="" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + break + fi + if [ "$status" = "pending" ]; then - _info "Pending" + _info "Pending, The CA is processing your order, please just wait. ($waittimes/$MAX_RETRY_TIMES)" elif [ "$status" = "processing" ]; then - _info "Processing" + _info "Processing, The CA is processing your order, please just wait. ($waittimes/$MAX_RETRY_TIMES)" else _err "$d:Verify error:$response" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" @@ -4708,7 +4878,19 @@ $_authorizations_map" _on_issue_err "$_post_hook" "$vlist" return 1 fi + _debug "sleep 2 secs to verify again" + sleep 2 + _debug "checking" + _send_signed_request "$uri" + + if [ "$?" != "0" ]; then + _err "$d:Verify error:$response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err "$_post_hook" "$vlist" + return 1 + fi done done @@ -4717,138 +4899,129 @@ $_authorizations_map" _info "Verify finished, start to sign." der="$(_getfile "${CSR_PATH}" "${BEGIN_CSR}" "${END_CSR}" | tr -d "\r\n" | _url_replace)" - if [ "$ACME_VERSION" = "2" ]; then - _info "Lets finalize the order." - _info "Le_OrderFinalize" "$Le_OrderFinalize" - if ! _send_signed_request "${Le_OrderFinalize}" "{\"csr\": \"$der\"}"; then - _err "Sign failed." - _on_issue_err "$_post_hook" - return 1 - fi - if [ "$code" != "200" ]; then - _err "Sign failed, finalize code is not 200." - _err "$response" - _on_issue_err "$_post_hook" - return 1 - fi - if [ -z "$Le_LinkOrder" ]; then - Le_LinkOrder="$(echo "$responseHeaders" | grep -i '^Location.*$' | _tail_n 1 | tr -d "\r\n \t" | cut -d ":" -f 2-)" - fi + _info "Lets finalize the order." + _info "Le_OrderFinalize" "$Le_OrderFinalize" + if ! _send_signed_request "${Le_OrderFinalize}" "{\"csr\": \"$der\"}"; then + _err "Sign failed." + _on_issue_err "$_post_hook" + return 1 + fi + if [ "$code" != "200" ]; then + _err "Sign failed, finalize code is not 200." + _err "$response" + _on_issue_err "$_post_hook" + return 1 + fi + if [ -z "$Le_LinkOrder" ]; then + Le_LinkOrder="$(echo "$responseHeaders" | grep -i '^Location.*$' | _tail_n 1 | tr -d "\r\n \t" | cut -d ":" -f 2-)" + fi - _savedomainconf "Le_LinkOrder" "$Le_LinkOrder" + _savedomainconf "Le_LinkOrder" "$Le_LinkOrder" - _link_cert_retry=0 - _MAX_CERT_RETRY=30 - while [ "$_link_cert_retry" -lt "$_MAX_CERT_RETRY" ]; do - if _contains "$response" "\"status\":\"valid\""; then - _debug "Order status is valid." - Le_LinkCert="$(echo "$response" | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)" - _debug Le_LinkCert "$Le_LinkCert" - if [ -z "$Le_LinkCert" ]; then - _err "Sign error, can not find Le_LinkCert" - _err "$response" - _on_issue_err "$_post_hook" - return 1 - fi - break - elif _contains "$response" "\"processing\""; then - _info "Order status is processing, lets sleep and retry." - _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') - _debug "_retryafter" "$_retryafter" - if [ "$_retryafter" ]; then - _info "Retry after: $_retryafter" - _sleep $_retryafter - else - _sleep 2 - fi + _link_cert_retry=0 + _MAX_CERT_RETRY=30 + while [ "$_link_cert_retry" -lt "$_MAX_CERT_RETRY" ]; do + if _contains "$response" "\"status\":\"valid\""; then + _debug "Order status is valid." + Le_LinkCert="$(echo "$response" | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)" + _debug Le_LinkCert "$Le_LinkCert" + if [ -z "$Le_LinkCert" ]; then + _err "Sign error, can not find Le_LinkCert" + _err "$response" + _on_issue_err "$_post_hook" + return 1 + fi + break + elif _contains "$response" "\"processing\""; then + _info "Order status is processing, lets sleep and retry." + _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') + _debug "_retryafter" "$_retryafter" + if [ "$_retryafter" ]; then + _info "Retry after: $_retryafter" + _sleep $_retryafter else - _err "Sign error, wrong status" - _err "$response" - _on_issue_err "$_post_hook" - return 1 + _sleep 2 fi - #the order is processing, so we are going to poll order status - if [ -z "$Le_LinkOrder" ]; then - _err "Sign error, can not get order link location header" - _err "responseHeaders" "$responseHeaders" - _on_issue_err "$_post_hook" - return 1 - fi - _info "Polling order status: $Le_LinkOrder" - if ! _send_signed_request "$Le_LinkOrder"; then - _err "Sign failed, can not post to Le_LinkOrder cert:$Le_LinkOrder." - _err "$response" - _on_issue_err "$_post_hook" - return 1 - fi - _link_cert_retry="$(_math $_link_cert_retry + 1)" - done - - if [ -z "$Le_LinkCert" ]; then - _err "Sign failed, can not get Le_LinkCert, retry time limit." + else + _err "Sign error, wrong status" _err "$response" _on_issue_err "$_post_hook" return 1 fi - _info "Downloading cert." - _info "Le_LinkCert" "$Le_LinkCert" - if ! _send_signed_request "$Le_LinkCert"; then - _err "Sign failed, can not download cert:$Le_LinkCert." + #the order is processing, so we are going to poll order status + if [ -z "$Le_LinkOrder" ]; then + _err "Sign error, can not get order link location header" + _err "responseHeaders" "$responseHeaders" + _on_issue_err "$_post_hook" + return 1 + fi + _info "Polling order status: $Le_LinkOrder" + if ! _send_signed_request "$Le_LinkOrder"; then + _err "Sign failed, can not post to Le_LinkOrder cert:$Le_LinkOrder." _err "$response" _on_issue_err "$_post_hook" return 1 fi + _link_cert_retry="$(_math $_link_cert_retry + 1)" + done - echo "$response" >"$CERT_PATH" - _split_cert_chain "$CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CA_CERT_PATH" + if [ -z "$Le_LinkCert" ]; then + _err "Sign failed, can not get Le_LinkCert, retry time limit." + _err "$response" + _on_issue_err "$_post_hook" + return 1 + fi + _info "Downloading cert." + _info "Le_LinkCert" "$Le_LinkCert" + if ! _send_signed_request "$Le_LinkCert"; then + _err "Sign failed, can not download cert:$Le_LinkCert." + _err "$response" + _on_issue_err "$_post_hook" + return 1 + fi - if [ "$_preferred_chain" ] && [ -f "$CERT_FULLCHAIN_PATH" ]; then - if ! _match_issuer "$CERT_FULLCHAIN_PATH" "$_preferred_chain"; then - rels="$(echo "$responseHeaders" | tr -d ' <>' | grep -i "^link:" | grep -i 'rel="alternate"' | cut -d : -f 2- | cut -d ';' -f 1)" - _debug2 "rels" "$rels" - for rel in $rels; do - _info "Try rel: $rel" - if ! _send_signed_request "$rel"; then - _err "Sign failed, can not download cert:$rel" - _err "$response" - continue - fi - _relcert="$CERT_PATH.alt" - _relfullchain="$CERT_FULLCHAIN_PATH.alt" - _relca="$CA_CERT_PATH.alt" - echo "$response" >"$_relcert" - _split_cert_chain "$_relcert" "$_relfullchain" "$_relca" - if _match_issuer "$_relfullchain" "$_preferred_chain"; then - _info "Matched issuer in: $rel" - cat $_relcert >"$CERT_PATH" - cat $_relfullchain >"$CERT_FULLCHAIN_PATH" - cat $_relca >"$CA_CERT_PATH" - break - fi - done - fi + 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) + fi + if [ "$_preferred_chain" ] && [ -f "$CERT_FULLCHAIN_PATH" ]; then + if [ "$DEBUG" ]; then + _debug "default chain issuers: " "$(_get_chain_issuers "$CERT_FULLCHAIN_PATH")" fi - else - if ! _send_signed_request "${ACME_NEW_ORDER}" "{\"resource\": \"$ACME_NEW_ORDER_RES\", \"csr\": \"$der\"}" "needbase64"; then - _err "Sign failed. $response" - _on_issue_err "$_post_hook" - return 1 + if ! _match_issuer "$CERT_FULLCHAIN_PATH" "$_preferred_chain"; then + rels="$(echo "$responseHeaders" | tr -d ' <>' | grep -i "^link:" | grep -i 'rel="alternate"' | cut -d : -f 2- | cut -d ';' -f 1)" + _debug2 "rels" "$rels" + for rel in $rels; do + _info "Try rel: $rel" + if ! _send_signed_request "$rel"; then + _err "Sign failed, can not download cert:$rel" + _err "$response" + continue + fi + _relcert="$CERT_PATH.alt" + _relfullchain="$CERT_FULLCHAIN_PATH.alt" + _relca="$CA_CERT_PATH.alt" + echo "$response" >"$_relcert" + _split_cert_chain "$_relcert" "$_relfullchain" "$_relca" + if [ "$DEBUG" ]; then + _debug "rel chain issuers: " "$(_get_chain_issuers "$_relfullchain")" + fi + if _match_issuer "$_relfullchain" "$_preferred_chain"; then + _info "Matched issuer in: $rel" + cat $_relcert >"$CERT_PATH" + cat $_relfullchain >"$CERT_FULLCHAIN_PATH" + cat $_relca >"$CA_CERT_PATH" + rm -f "$_relcert" + rm -f "$_relfullchain" + rm -f "$_relca" + break + fi + rm -f "$_relcert" + rm -f "$_relfullchain" + rm -f "$_relca" + done fi - _rcert="$response" - Le_LinkCert="$(grep -i '^Location.*$' "$HTTP_HEADER" | _tail_n 1 | tr -d "\r\n" | cut -d " " -f 2)" - echo "$BEGIN_CERT" >"$CERT_PATH" - - #if ! _get "$Le_LinkCert" | _base64 "multiline" >> "$CERT_PATH" ; then - # _debug "Get cert failed. Let's try last response." - # printf -- "%s" "$_rcert" | _dbase64 "multiline" | _base64 "multiline" >> "$CERT_PATH" - #fi - - if ! printf -- "%s" "$_rcert" | _dbase64 "multiline" | _base64 "multiline" >>"$CERT_PATH"; then - _debug "Try cert link." - _get "$Le_LinkCert" | _base64 "multiline" >>"$CERT_PATH" - fi - - echo "$END_CERT" >>"$CERT_PATH" fi _debug "Le_LinkCert" "$Le_LinkCert" @@ -4865,10 +5038,10 @@ $_authorizations_map" _info "$(__green "Cert success.")" cat "$CERT_PATH" - _info "Your cert is in $(__green " $CERT_PATH ")" + _info "Your cert is in: $(__green "$CERT_PATH")" if [ -f "$CERT_KEY_PATH" ]; then - _info "Your cert key is in $(__green " $CERT_KEY_PATH ")" + _info "Your cert key is in: $(__green "$CERT_KEY_PATH")" fi if [ ! "$USER_PATH" ] || [ ! "$_ACME_IN_CRON" ]; then @@ -4877,55 +5050,8 @@ $_authorizations_map" fi fi - if [ "$ACME_VERSION" = "2" ]; then - _debug "v2 chain." - else - cp "$CERT_PATH" "$CERT_FULLCHAIN_PATH" - Le_LinkIssuer=$(grep -i '^Link' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | cut -d ';' -f 1 | tr -d '<>') - - if [ "$Le_LinkIssuer" ]; then - if ! _contains "$Le_LinkIssuer" ":"; then - _info "$(__red "Relative issuer link found.")" - Le_LinkIssuer="$_ACME_SERVER_HOST$Le_LinkIssuer" - fi - _debug Le_LinkIssuer "$Le_LinkIssuer" - _savedomainconf "Le_LinkIssuer" "$Le_LinkIssuer" - - _link_issuer_retry=0 - _MAX_ISSUER_RETRY=5 - while [ "$_link_issuer_retry" -lt "$_MAX_ISSUER_RETRY" ]; do - _debug _link_issuer_retry "$_link_issuer_retry" - if [ "$ACME_VERSION" = "2" ]; then - if _send_signed_request "$Le_LinkIssuer"; then - echo "$response" >"$CA_CERT_PATH" - break - fi - else - if _get "$Le_LinkIssuer" >"$CA_CERT_PATH.der"; then - echo "$BEGIN_CERT" >"$CA_CERT_PATH" - _base64 "multiline" <"$CA_CERT_PATH.der" >>"$CA_CERT_PATH" - echo "$END_CERT" >>"$CA_CERT_PATH" - if ! _checkcert "$CA_CERT_PATH"; then - _err "Can not get the ca cert." - break - fi - cat "$CA_CERT_PATH" >>"$CERT_FULLCHAIN_PATH" - rm -f "$CA_CERT_PATH.der" - break - fi - fi - _link_issuer_retry=$(_math $_link_issuer_retry + 1) - _sleep "$_link_issuer_retry" - done - if [ "$_link_issuer_retry" = "$_MAX_ISSUER_RETRY" ]; then - _err "Max retry for issuer ca cert is reached." - fi - else - _debug "No Le_LinkIssuer header found." - fi - fi - [ -f "$CA_CERT_PATH" ] && _info "The intermediate CA cert is in $(__green " $CA_CERT_PATH ")" - [ -f "$CERT_FULLCHAIN_PATH" ] && _info "And the full chain certs is there: $(__green " $CERT_FULLCHAIN_PATH ")" + [ -f "$CA_CERT_PATH" ] && _info "The intermediate CA cert is in: $(__green "$CA_CERT_PATH")" + [ -f "$CERT_FULLCHAIN_PATH" ] && _info "And the full chain certs is there: $(__green "$CERT_FULLCHAIN_PATH")" Le_CertCreateTime=$(_time) _savedomainconf "Le_CertCreateTime" "$Le_CertCreateTime" @@ -5036,17 +5162,16 @@ renew() { . "$DOMAIN_CONF" _debug Le_API "$Le_API" - - if [ "$Le_API" = "$LETSENCRYPT_CA_V1" ]; then - _cleardomainconf Le_API - Le_API="$DEFAULT_CA" - fi - if [ "$Le_API" = "$LETSENCRYPT_STAGING_CA_V1" ]; then - _cleardomainconf Le_API - Le_API="$DEFAULT_STAGING_CA" + if [ -z "$Le_API" ] || [ "$CA_LETSENCRYPT_V1" = "$Le_API" ]; then + #if this is from an old version, Le_API is empty, + #so, we force to use letsencrypt server + Le_API="$CA_LETSENCRYPT_V2" fi if [ "$Le_API" ]; then + if [ "$Le_API" != "$ACME_DIRECTORY" ]; then + _clearAPI + fi export ACME_DIRECTORY="$Le_API" #reload ca configs ACCOUNT_KEY_PATH="" @@ -5222,6 +5347,7 @@ signcsr() { _renew_hook="${10}" _local_addr="${11}" _challenge_alias="${12}" + _preferred_chain="${13}" _csrsubj=$(_readSubjectFromCSR "$_csrfile") if [ "$?" != "0" ]; then @@ -5259,16 +5385,13 @@ signcsr() { return 1 fi - if [ -z "$ACME_VERSION" ] && _contains "$_csrsubj,$_csrdomainlist" "*."; then - export ACME_VERSION=2 - fi _initpath "$_csrsubj" "$_csrkeylength" mkdir -p "$DOMAIN_PATH" _info "Copy csr to: $CSR_PATH" cp "$_csrfile" "$CSR_PATH" - issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" + issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain" } @@ -5471,7 +5594,7 @@ _installcert() { mkdir -p "$_backup_path" if [ "$_real_cert" ]; then - _info "Installing cert to:$_real_cert" + _info "Installing cert to: $_real_cert" if [ -f "$_real_cert" ] && [ ! "$_ACME_IS_RENEW" ]; then cp "$_real_cert" "$_backup_path/cert.bak" fi @@ -5479,7 +5602,7 @@ _installcert() { fi if [ "$_real_ca" ]; then - _info "Installing CA to:$_real_ca" + _info "Installing CA to: $_real_ca" if [ "$_real_ca" = "$_real_cert" ]; then echo "" >>"$_real_ca" cat "$CA_CERT_PATH" >>"$_real_ca" || return 1 @@ -5492,7 +5615,7 @@ _installcert() { fi if [ "$_real_key" ]; then - _info "Installing key to:$_real_key" + _info "Installing key to: $_real_key" if [ -f "$_real_key" ] && [ ! "$_ACME_IS_RENEW" ]; then cp "$_real_key" "$_backup_path/key.bak" fi @@ -5505,7 +5628,7 @@ _installcert() { fi if [ "$_real_fullchain" ]; then - _info "Installing full chain to:$_real_fullchain" + _info "Installing full chain to: $_real_fullchain" if [ -f "$_real_fullchain" ] && [ ! "$_ACME_IS_RENEW" ]; then cp "$_real_fullchain" "$_backup_path/fullchain.bak" fi @@ -5600,8 +5723,16 @@ installcronjob() { if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY" ]; then lesh="\"$LE_WORKING_DIR\"/$PROJECT_ENTRY" else - _err "Can not install cronjob, $PROJECT_ENTRY not found." - return 1 + _debug "_SCRIPT_" "$_SCRIPT_" + _script="$(_readlink "$_SCRIPT_")" + _debug _script "$_script" + if [ -f "$_script" ]; then + _info "Using the current script from: $_script" + lesh="$_script" + else + _err "Can not install cronjob, $PROJECT_ENTRY not found." + return 1 + fi fi if [ "$_c_home" ]; then _c_entry="--config-home \"$_c_home\" " @@ -5673,7 +5804,7 @@ uninstallcronjob() { _info "Removing cron job" cr="$($_CRONTAB -l | grep "$PROJECT_ENTRY --cron")" if [ "$cr" ]; then - if _exists uname && uname -a | grep solaris >/dev/null; then + if _exists uname && uname -a | grep SunOS >/dev/null; then $_CRONTAB -l | sed "/$PROJECT_ENTRY --cron/d" | $_CRONTAB -- else $_CRONTAB -l | sed "/$PROJECT_ENTRY --cron/d" | $_CRONTAB - @@ -5713,6 +5844,23 @@ revoke() { return 1 fi + . "$DOMAIN_CONF" + _debug Le_API "$Le_API" + + if [ "$Le_API" ]; then + if [ "$Le_API" != "$ACME_DIRECTORY" ]; then + _clearAPI + fi + export ACME_DIRECTORY="$Le_API" + #reload ca configs + ACCOUNT_KEY_PATH="" + ACCOUNT_JSON_PATH="" + CA_CONF="" + _debug3 "initpath again." + _initpath "$Le_Domain" "$_isEcc" + _initAPI + fi + cert="$(_getfile "${CERT_PATH}" "${BEGIN_CERT}" "${END_CERT}" | tr -d "\r\n" | _url_replace)" if [ -z "$cert" ]; then @@ -5722,11 +5870,8 @@ revoke() { _initAPI - if [ "$ACME_VERSION" = "2" ]; then - data="{\"certificate\": \"$cert\",\"reason\":$_reason}" - else - data="{\"resource\": \"revoke-cert\", \"certificate\": \"$cert\"}" - fi + data="{\"certificate\": \"$cert\",\"reason\":$_reason}" + uri="${ACME_REVOKE_CERT}" if [ -f "$CERT_KEY_PATH" ]; then @@ -5795,60 +5940,62 @@ remove() { _deactivate() { _d_domain="$1" _d_type="$2" - _initpath + _initpath "$_d_domain" "$_d_type" - if [ "$ACME_VERSION" = "2" ]; then - _identifiers="{\"type\":\"dns\",\"value\":\"$_d_domain\"}" - if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then - _err "Can not get domain new order." - return 1 - fi - _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." - _clearup - _on_issue_err "$_post_hook" - return 1 - fi + . "$DOMAIN_CONF" + _debug Le_API "$Le_API" - authzUri="$_authorizations_seg" - _debug2 "authzUri" "$authzUri" - if ! _send_signed_request "$authzUri"; then - _err "get to authz error." - _err "_authorizations_seg" "$_authorizations_seg" - _err "authzUri" "$authzUri" - _clearup - _on_issue_err "$_post_hook" - return 1 + if [ "$Le_API" ]; then + if [ "$Le_API" != "$ACME_DIRECTORY" ]; then + _clearAPI fi - - response="$(echo "$response" | _normalizeJson)" - _debug2 response "$response" - _URL_NAME="url" - else - if ! __get_domain_new_authz "$_d_domain"; then - _err "Can not get domain new authz token." - return 1 - fi - - authzUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n")" - _debug "authzUri" "$authzUri" - if [ "$code" ] && [ ! "$code" = '201' ]; then - _err "new-authz error: $response" - return 1 - fi - _URL_NAME="uri" + export ACME_DIRECTORY="$Le_API" + #reload ca configs + ACCOUNT_KEY_PATH="" + ACCOUNT_JSON_PATH="" + CA_CONF="" + _debug3 "initpath again." + _initpath "$Le_Domain" "$_d_type" + _initAPI fi - entries="$(echo "$response" | tr '][' '==' | _egrep_o "challenges\": *=[^=]*=" | tr '}{' '\n' | grep "\"status\": *\"valid\"")" + _identifiers="{\"type\":\"$(_getIdType "$_d_domain")\",\"value\":\"$_d_domain\"}" + if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then + _err "Can not get domain new order." + return 1 + fi + _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." + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + authzUri="$_authorizations_seg" + _debug2 "authzUri" "$authzUri" + if ! _send_signed_request "$authzUri"; then + _err "get to authz error." + _err "_authorizations_seg" "$_authorizations_seg" + _err "authzUri" "$authzUri" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + + response="$(echo "$response" | _normalizeJson)" + _debug2 response "$response" + _URL_NAME="url" + + entries="$(echo "$response" | tr '][' '==' | _egrep_o "challenges\": *=[^=]*=" | tr '}{' '\n\n' | grep "\"status\": *\"valid\"")" if [ -z "$entries" ]; then _info "No valid entries found." if [ -z "$thumbprint" ]; then thumbprint="$(__calc_account_thumbprint)" fi _debug "Trigger validation." - vtype="$VTYPE_DNS" + vtype="$(_getIdType "$_d_domain")" entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$entry" ]; then @@ -5894,11 +6041,7 @@ _deactivate() { _info "Deactivate: $_vtype" - if [ "$ACME_VERSION" = "2" ]; then - _djson="{\"status\":\"deactivated\"}" - else - _djson="{\"resource\": \"authz\", \"status\":\"deactivated\"}" - fi + _djson="{\"status\":\"deactivated\"}" if _send_signed_request "$authzUri" "$_djson" && _contains "$response" '"deactivated"'; then _info "Deactivate: $_vtype success." @@ -6461,6 +6604,8 @@ Commands: --deactivate Deactivate the domain authz, professional use. --set-default-ca Used with '--server', Set the default CA to use. See: $_SERVER_WIKI + --set-default-chain Set the default preferred chain for a CA. + See: $_PREFERRED_CHAIN_WIKI Parameters: @@ -6609,7 +6754,7 @@ _getRepoHash() { _hash_path=$1 shift _hash_url="https://api.github.com/repos/acmesh-official/$PROJECT_NAME/git/refs/$_hash_path" - _get $_hash_url | tr -d "\r\n" | tr '{},' '\n' | grep '"sha":' | cut -d '"' -f 4 + _get $_hash_url | tr -d "\r\n" | tr '{},' '\n\n\n' | grep '"sha":' | cut -d '"' -f 4 } _getUpgradeHash() { @@ -6682,9 +6827,10 @@ _checkSudo() { return 0 } -#server +#server #keylength _selectServer() { _server="$1" + _skeylength="$2" _server_lower="$(echo "$_server" | _lower_case)" _sindex=0 for snames in $CA_NAMES; do @@ -6695,6 +6841,9 @@ _selectServer() { if [ "$_server_lower" = "$sname" ]; then _debug2 "_selectServer match $sname" _serverdir="$(_getfield "$CA_SERVERS" $_sindex)" + if [ "$_serverdir" = "$CA_SSLCOM_RSA" ] && _isEccKey "$_skeylength"; then + _serverdir="$CA_SSLCOM_ECC" + fi _debug "Selected server: $_serverdir" ACME_DIRECTORY="$_serverdir" export ACME_DIRECTORY @@ -6712,6 +6861,9 @@ _getCAShortName() { if [ -z "$caurl" ]; then caurl="$DEFAULT_CA" fi + if [ "$CA_SSLCOM_ECC" = "$caurl" ]; then + caurl="$CA_SSLCOM_RSA" #just hack to get the short name + fi caurl_lower="$(echo $caurl | _lower_case)" _sindex=0 for surl in $(echo "$CA_SERVERS" | _lower_case | tr , ' '); do @@ -6740,6 +6892,18 @@ setdefaultca() { _info "Changed default CA to: $(__green "$ACME_DIRECTORY")" } +#preferred-chain +setdefaultchain() { + _initpath + _preferred_chain="$1" + if [ -z "$_preferred_chain" ]; then + _err "Please give a '--preferred-chain value' value." + return 1 + fi + mkdir -p "$CA_DIR" + _savecaconf "DEFAULT_PREFERRED_CHAIN" "$_preferred_chain" +} + _process() { _CMD="" _domain="" @@ -6891,6 +7055,9 @@ _process() { --set-default-ca) _CMD="setdefaultca" ;; + --set-default-chain) + _CMD="setdefaultchain" + ;; -d | --domain) _dvalue="$2" @@ -6904,10 +7071,6 @@ _process() { return 1 fi - if _startswith "$_dvalue" "*."; then - _debug "Wildcard domain" - export ACME_VERSION=2 - fi if [ -z "$_domain" ]; then _domain="$_dvalue" else @@ -6930,7 +7093,6 @@ _process() { ;; --server) _server="$2" - _selectServer "$_server" shift ;; --debug) @@ -7029,7 +7191,6 @@ _process() { Le_DNSSleep="$_dnssleep" shift ;; - --keylength | -k) _keylength="$2" shift @@ -7038,7 +7199,6 @@ _process() { _accountkeylength="$2" shift ;; - --cert-file | --certpath) _cert_file="$2" shift @@ -7302,6 +7462,10 @@ _process() { shift 1 done + if [ "$_server" ]; then + _selectServer "$_server" "${_ecc:-$_keylength}" + fi + if [ "${_CMD}" != "install" ]; then if [ "$__INTERACTIVE" ] && ! _checkSudo; then if [ -z "$FORCE" ]; then @@ -7365,7 +7529,7 @@ _process() { deploy "$_domain" "$_deploy_hook" "$_ecc" ;; signcsr) - signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" + signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" ;; showcsr) showcsr "$_csr" "$_domain" @@ -7424,6 +7588,9 @@ _process() { setdefaultca) setdefaultca ;; + setdefaultchain) + setdefaultchain "$_preferred_chain" + ;; *) if [ "$_CMD" ]; then _err "Invalid command: $_CMD" diff --git a/deploy/cleverreach.sh b/deploy/cleverreach.sh index 552d8149..a460a139 100644 --- a/deploy/cleverreach.sh +++ b/deploy/cleverreach.sh @@ -17,6 +17,8 @@ cleverreach_deploy() { _cca="$4" _cfullchain="$5" + _rest_endpoint="https://rest.cleverreach.com" + _debug _cdomain "$_cdomain" _debug _ckey "$_ckey" _debug _ccert "$_ccert" @@ -25,6 +27,7 @@ cleverreach_deploy() { _getdeployconf DEPLOY_CLEVERREACH_CLIENT_ID _getdeployconf DEPLOY_CLEVERREACH_CLIENT_SECRET + _getdeployconf DEPLOY_CLEVERREACH_SUBCLIENT_ID if [ -z "${DEPLOY_CLEVERREACH_CLIENT_ID}" ]; then _err "CleverReach Client ID is not found, please define DEPLOY_CLEVERREACH_CLIENT_ID." @@ -37,11 +40,12 @@ cleverreach_deploy() { _savedeployconf DEPLOY_CLEVERREACH_CLIENT_ID "${DEPLOY_CLEVERREACH_CLIENT_ID}" _savedeployconf DEPLOY_CLEVERREACH_CLIENT_SECRET "${DEPLOY_CLEVERREACH_CLIENT_SECRET}" + _savedeployconf DEPLOY_CLEVERREACH_SUBCLIENT_ID "${DEPLOY_CLEVERREACH_SUBCLIENT_ID}" _info "Obtaining a CleverReach access token" _data="{\"grant_type\": \"client_credentials\", \"client_id\": \"${DEPLOY_CLEVERREACH_CLIENT_ID}\", \"client_secret\": \"${DEPLOY_CLEVERREACH_CLIENT_SECRET}\"}" - _auth_result="$(_post "$_data" "https://rest.cleverreach.com/oauth/token.php" "" "POST" "application/json")" + _auth_result="$(_post "$_data" "$_rest_endpoint/oauth/token.php" "" "POST" "application/json")" _debug _data "$_data" _debug _auth_result "$_auth_result" @@ -50,14 +54,32 @@ cleverreach_deploy() { _debug _regex "$_regex" _access_token=$(echo "$_auth_result" | _json_decode | sed -n "s/$_regex/\1/p") + _debug _subclient "${DEPLOY_CLEVERREACH_SUBCLIENT_ID}" + + if [ -n "${DEPLOY_CLEVERREACH_SUBCLIENT_ID}" ]; then + _info "Obtaining token for sub-client ${DEPLOY_CLEVERREACH_SUBCLIENT_ID}" + export _H1="Authorization: Bearer ${_access_token}" + _subclient_token_result="$(_get "$_rest_endpoint/v3/clients/$DEPLOY_CLEVERREACH_SUBCLIENT_ID/token")" + _access_token=$(echo "$_subclient_token_result" | sed -n "s/\"//p") + + _debug _subclient_token_result "$_access_token" + + _info "Destroying parent token at CleverReach, as it not needed anymore" + _destroy_result="$(_post "" "$_rest_endpoint/v3/oauth/token.json" "" "DELETE" "application/json")" + _debug _destroy_result "$_destroy_result" + fi + _info "Uploading certificate and key to CleverReach" _certData="{\"cert\":\"$(_json_encode <"$_cfullchain")\", \"key\":\"$(_json_encode <"$_ckey")\"}" export _H1="Authorization: Bearer ${_access_token}" - _add_cert_result="$(_post "$_certData" "https://rest.cleverreach.com/v3/ssl" "" "POST" "application/json")" + _add_cert_result="$(_post "$_certData" "$_rest_endpoint/v3/ssl" "" "POST" "application/json")" - _debug "Destroying token at CleverReach" - _post "" "https://rest.cleverreach.com/v3/oauth/token.json" "" "DELETE" "application/json" + if [ -z "${DEPLOY_CLEVERREACH_SUBCLIENT_ID}" ]; then + _info "Destroying token at CleverReach, as it not needed anymore" + _destroy_result="$(_post "" "$_rest_endpoint/v3/oauth/token.json" "" "DELETE" "application/json")" + _debug _destroy_result "$_destroy_result" + fi if ! echo "$_add_cert_result" | grep '"error":' >/dev/null; then _info "Uploaded certificate successfully" diff --git a/deploy/consul.sh b/deploy/consul.sh new file mode 100644 index 00000000..f93fb452 --- /dev/null +++ b/deploy/consul.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env sh + +# Here is a script to deploy cert to hashicorp consul using curl +# (https://www.consul.io/) +# +# it requires following environment variables: +# +# CONSUL_PREFIX - this contains the prefix path in consul +# CONSUL_HTTP_ADDR - consul requires this to find your consul server +# +# additionally, you need to ensure that CONSUL_HTTP_TOKEN is available +# to access the consul server + +#returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +consul_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" + + # validate required env vars + _getdeployconf CONSUL_PREFIX + if [ -z "$CONSUL_PREFIX" ]; then + _err "CONSUL_PREFIX needs to be defined (contains prefix path in vault)" + return 1 + fi + _savedeployconf CONSUL_PREFIX "$CONSUL_PREFIX" + + _getdeployconf CONSUL_HTTP_ADDR + if [ -z "$CONSUL_HTTP_ADDR" ]; then + _err "CONSUL_HTTP_ADDR needs to be defined (contains consul connection address)" + return 1 + fi + _savedeployconf CONSUL_HTTP_ADDR "$CONSUL_HTTP_ADDR" + + CONSUL_CMD=$(command -v consul) + + # force CLI, but the binary does not exist => error + if [ -n "$USE_CLI" ] && [ -z "$CONSUL_CMD" ]; then + _err "Cannot find the consul binary!" + return 1 + fi + + # use the CLI first + if [ -n "$USE_CLI" ] || [ -n "$CONSUL_CMD" ]; then + _info "Found consul binary, deploying with CLI" + consul_deploy_cli "$CONSUL_CMD" "$CONSUL_PREFIX" + else + _info "Did not find consul binary, deploying with API" + consul_deploy_api "$CONSUL_HTTP_ADDR" "$CONSUL_PREFIX" "$CONSUL_HTTP_TOKEN" + fi +} + +consul_deploy_api() { + CONSUL_HTTP_ADDR="$1" + CONSUL_PREFIX="$2" + CONSUL_HTTP_TOKEN="$3" + + URL="$CONSUL_HTTP_ADDR/v1/kv/$CONSUL_PREFIX" + export _H1="X-Consul-Token: $CONSUL_HTTP_TOKEN" + + if [ -n "$FABIO" ]; then + _post "$(cat "$_cfullchain")" "$URL/${_cdomain}-cert.pem" '' "PUT" || return 1 + _post "$(cat "$_ckey")" "$URL/${_cdomain}-key.pem" '' "PUT" || return 1 + else + _post "$(cat "$_ccert")" "$URL/${_cdomain}/cert.pem" '' "PUT" || return 1 + _post "$(cat "$_ckey")" "$URL/${_cdomain}/cert.key" '' "PUT" || return 1 + _post "$(cat "$_cca")" "$URL/${_cdomain}/chain.pem" '' "PUT" || return 1 + _post "$(cat "$_cfullchain")" "$URL/${_cdomain}/fullchain.pem" '' "PUT" || return 1 + fi +} + +consul_deploy_cli() { + CONSUL_CMD="$1" + CONSUL_PREFIX="$2" + + if [ -n "$FABIO" ]; then + $CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}-cert.pem" @"$_cfullchain" || return 1 + $CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}-key.pem" @"$_ckey" || return 1 + else + $CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1 + $CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1 + $CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1 + $CONSUL_CMD kv put "${CONSUL_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1 + fi +} diff --git a/deploy/gcore_cdn.sh b/deploy/gcore_cdn.sh index a2a35f7b..f573a3aa 100644 --- a/deploy/gcore_cdn.sh +++ b/deploy/gcore_cdn.sh @@ -56,9 +56,9 @@ gcore_cdn_deploy() { _request="{\"username\":\"$Le_Deploy_gcore_cdn_username\",\"password\":\"$Le_Deploy_gcore_cdn_password\"}" _debug _request "$_request" export _H1="Content-Type:application/json" - _response=$(_post "$_request" "https://api.gcdn.co/auth/signin") + _response=$(_post "$_request" "https://api.gcdn.co/auth/jwt/login") _debug _response "$_response" - _regex=".*\"token\":\"\([-._0-9A-Za-z]*\)\".*$" + _regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$" _debug _regex "$_regex" _token=$(echo "$_response" | sed -n "s/$_regex/\1/p") _debug _token "$_token" @@ -72,12 +72,15 @@ gcore_cdn_deploy() { export _H2="Authorization:Token $_token" _response=$(_get "https://api.gcdn.co/resources") _debug _response "$_response" - _regex=".*(\"id\".*?\"cname\":\"$_cdomain\".*?})" + _regex="\"primary_resource\":null}," + _debug _regex "$_regex" + _response=$(echo "$_response" | sed "s/$_regex/$_regex\n/g") + _debug _response "$_response" _regex="^.*\"cname\":\"$_cdomain\".*$" _debug _regex "$_regex" - _resource=$(echo "$_response" | sed 's/},{/},\n{/g' | _egrep_o "$_regex") + _resource=$(echo "$_response" | _egrep_o "$_regex") _debug _resource "$_resource" - _regex=".*\"id\":\([0-9]*\).*\"rules\".*$" + _regex=".*\"id\":\([0-9]*\).*$" _debug _regex "$_regex" _resourceId=$(echo "$_resource" | sed -n "s/$_regex/\1/p") _debug _resourceId "$_resourceId" diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index 0a45ee07..c255059d 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -54,11 +54,6 @@ haproxy_deploy() { DEPLOY_HAPROXY_ISSUER_DEFAULT="no" DEPLOY_HAPROXY_RELOAD_DEFAULT="true" - if [ -f "${DOMAIN_CONF}" ]; then - # shellcheck disable=SC1090 - . "${DOMAIN_CONF}" - fi - _debug _cdomain "${_cdomain}" _debug _ckey "${_ckey}" _debug _ccert "${_ccert}" @@ -66,6 +61,8 @@ haproxy_deploy() { _debug _cfullchain "${_cfullchain}" # PEM_PATH is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_PATH_DEFAULT}" + _getdeployconf DEPLOY_HAPROXY_PEM_PATH + _debug2 DEPLOY_HAPROXY_PEM_PATH "${DEPLOY_HAPROXY_PEM_PATH}" if [ -n "${DEPLOY_HAPROXY_PEM_PATH}" ]; then Le_Deploy_haproxy_pem_path="${DEPLOY_HAPROXY_PEM_PATH}" _savedomainconf Le_Deploy_haproxy_pem_path "${Le_Deploy_haproxy_pem_path}" @@ -82,6 +79,8 @@ haproxy_deploy() { fi # PEM_NAME is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}" + _getdeployconf DEPLOY_HAPROXY_PEM_NAME + _debug2 DEPLOY_HAPROXY_PEM_NAME "${DEPLOY_HAPROXY_PEM_NAME}" if [ -n "${DEPLOY_HAPROXY_PEM_NAME}" ]; then Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME}" _savedomainconf Le_Deploy_haproxy_pem_name "${Le_Deploy_haproxy_pem_name}" @@ -90,6 +89,8 @@ haproxy_deploy() { fi # BUNDLE is optional. If not provided then assume "${DEPLOY_HAPROXY_BUNDLE_DEFAULT}" + _getdeployconf DEPLOY_HAPROXY_BUNDLE + _debug2 DEPLOY_HAPROXY_BUNDLE "${DEPLOY_HAPROXY_BUNDLE}" if [ -n "${DEPLOY_HAPROXY_BUNDLE}" ]; then Le_Deploy_haproxy_bundle="${DEPLOY_HAPROXY_BUNDLE}" _savedomainconf Le_Deploy_haproxy_bundle "${Le_Deploy_haproxy_bundle}" @@ -98,6 +99,8 @@ haproxy_deploy() { fi # ISSUER is optional. If not provided then assume "${DEPLOY_HAPROXY_ISSUER_DEFAULT}" + _getdeployconf DEPLOY_HAPROXY_ISSUER + _debug2 DEPLOY_HAPROXY_ISSUER "${DEPLOY_HAPROXY_ISSUER}" if [ -n "${DEPLOY_HAPROXY_ISSUER}" ]; then Le_Deploy_haproxy_issuer="${DEPLOY_HAPROXY_ISSUER}" _savedomainconf Le_Deploy_haproxy_issuer "${Le_Deploy_haproxy_issuer}" @@ -106,6 +109,8 @@ haproxy_deploy() { fi # RELOAD is optional. If not provided then assume "${DEPLOY_HAPROXY_RELOAD_DEFAULT}" + _getdeployconf DEPLOY_HAPROXY_RELOAD + _debug2 DEPLOY_HAPROXY_RELOAD "${DEPLOY_HAPROXY_RELOAD}" if [ -n "${DEPLOY_HAPROXY_RELOAD}" ]; then Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD}" _savedomainconf Le_Deploy_haproxy_reload "${Le_Deploy_haproxy_reload}" @@ -190,7 +195,7 @@ haproxy_deploy() { _info "Updating OCSP stapling info" _debug _ocsp "${_ocsp}" _info "Extracting OCSP URL" - _ocsp_url=$(openssl x509 -noout -ocsp_uri -in "${_pem}") + _ocsp_url=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -ocsp_uri -in "${_pem}") _debug _ocsp_url "${_ocsp_url}" # Only process OCSP if URL was present @@ -203,9 +208,9 @@ haproxy_deploy() { # Only process the certificate if we have a .issuer file if [ -r "${_issuer}" ]; then # Check if issuer cert is also a root CA cert - _subjectdn=$(openssl x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10) + _subjectdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10) _debug _subjectdn "${_subjectdn}" - _issuerdn=$(openssl x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10) + _issuerdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10) _debug _issuerdn "${_issuerdn}" _info "Requesting OCSP response" # If the issuer is a CA cert then our command line has "-CAfile" added @@ -216,7 +221,7 @@ haproxy_deploy() { fi _debug _cafile_argument "${_cafile_argument}" # if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed - _openssl_version=$(openssl version | cut -d' ' -f2) + _openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2) _debug _openssl_version "${_openssl_version}" _openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1) _openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2) @@ -226,7 +231,7 @@ haproxy_deploy() { _header_sep=" " fi # Request the OCSP response from the issuer and store it - _openssl_ocsp_cmd="openssl ocsp \ + _openssl_ocsp_cmd="${ACME_OPENSSL_BIN:-openssl} ocsp \ -issuer \"${_issuer}\" \ -cert \"${_pem}\" \ -url \"${_ocsp_url}\" \ diff --git a/deploy/kong.sh b/deploy/kong.sh index 1e1e310c..b8facedf 100755 --- a/deploy/kong.sh +++ b/deploy/kong.sh @@ -45,7 +45,7 @@ kong_deploy() { #Generate data for request (Multipart/form-data with mixed content) if [ -z "$ssl_uuid" ]; then #set sni to domain - content="--$delim${nl}Content-Disposition: form-data; name=\"snis\"${nl}${nl}$_cdomain" + content="--$delim${nl}Content-Disposition: form-data; name=\"snis[]\"${nl}${nl}$_cdomain" fi #add key content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"; filename=\"$(basename "$_ckey")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")" diff --git a/deploy/lighttpd.sh b/deploy/lighttpd.sh new file mode 100644 index 00000000..71f64b96 --- /dev/null +++ b/deploy/lighttpd.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env sh + +# Script for acme.sh to deploy certificates to lighttpd +# +# The following variables can be exported: +# +# export DEPLOY_LIGHTTPD_PEM_NAME="${domain}.pem" +# +# Defines the name of the PEM file. +# Defaults to ".pem" +# +# export DEPLOY_LIGHTTPD_PEM_PATH="/etc/lighttpd" +# +# Defines location of PEM file for Lighttpd. +# Defaults to /etc/lighttpd +# +# export DEPLOY_LIGHTTPD_RELOAD="systemctl reload lighttpd" +# +# OPTIONAL: Reload command used post deploy +# This defaults to be a no-op (ie "true"). +# It is strongly recommended to set this something that makes sense +# for your distro. +# +# export DEPLOY_LIGHTTPD_ISSUER="yes" +# +# OPTIONAL: Places CA file as "${DEPLOY_LIGHTTPD_PEM}.issuer" +# Note: Required for OCSP stapling to work +# +# export DEPLOY_LIGHTTPD_BUNDLE="no" +# +# OPTIONAL: Deploy this certificate as part of a multi-cert bundle +# This adds a suffix to the certificate based on the certificate type +# eg RSA certificates will have .rsa as a suffix to the file name +# Lighttpd will load all certificates and provide one or the other +# depending on client capabilities +# Note: This functionality requires Lighttpd was compiled against +# a version of OpenSSL that supports this. +# + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +lighttpd_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + + # Some defaults + DEPLOY_LIGHTTPD_PEM_PATH_DEFAULT="/etc/lighttpd" + DEPLOY_LIGHTTPD_PEM_NAME_DEFAULT="${_cdomain}.pem" + DEPLOY_LIGHTTPD_BUNDLE_DEFAULT="no" + DEPLOY_LIGHTTPD_ISSUER_DEFAULT="yes" + DEPLOY_LIGHTTPD_RELOAD_DEFAULT="true" + + _debug _cdomain "${_cdomain}" + _debug _ckey "${_ckey}" + _debug _ccert "${_ccert}" + _debug _cca "${_cca}" + _debug _cfullchain "${_cfullchain}" + + # PEM_PATH is optional. If not provided then assume "${DEPLOY_LIGHTTPD_PEM_PATH_DEFAULT}" + _getdeployconf DEPLOY_LIGHTTPD_PEM_PATH + _debug2 DEPLOY_LIGHTTPD_PEM_PATH "${DEPLOY_LIGHTTPD_PEM_PATH}" + if [ -n "${DEPLOY_LIGHTTPD_PEM_PATH}" ]; then + Le_Deploy_lighttpd_pem_path="${DEPLOY_LIGHTTPD_PEM_PATH}" + _savedomainconf Le_Deploy_lighttpd_pem_path "${Le_Deploy_lighttpd_pem_path}" + elif [ -z "${Le_Deploy_lighttpd_pem_path}" ]; then + Le_Deploy_lighttpd_pem_path="${DEPLOY_LIGHTTPD_PEM_PATH_DEFAULT}" + fi + + # Ensure PEM_PATH exists + if [ -d "${Le_Deploy_lighttpd_pem_path}" ]; then + _debug "PEM_PATH ${Le_Deploy_lighttpd_pem_path} exists" + else + _err "PEM_PATH ${Le_Deploy_lighttpd_pem_path} does not exist" + return 1 + fi + + # PEM_NAME is optional. If not provided then assume "${DEPLOY_LIGHTTPD_PEM_NAME_DEFAULT}" + _getdeployconf DEPLOY_LIGHTTPD_PEM_NAME + _debug2 DEPLOY_LIGHTTPD_PEM_NAME "${DEPLOY_LIGHTTPD_PEM_NAME}" + if [ -n "${DEPLOY_LIGHTTPD_PEM_NAME}" ]; then + Le_Deploy_lighttpd_pem_name="${DEPLOY_LIGHTTPD_PEM_NAME}" + _savedomainconf Le_Deploy_lighttpd_pem_name "${Le_Deploy_lighttpd_pem_name}" + elif [ -z "${Le_Deploy_lighttpd_pem_name}" ]; then + Le_Deploy_lighttpd_pem_name="${DEPLOY_LIGHTTPD_PEM_NAME_DEFAULT}" + fi + + # BUNDLE is optional. If not provided then assume "${DEPLOY_LIGHTTPD_BUNDLE_DEFAULT}" + _getdeployconf DEPLOY_LIGHTTPD_BUNDLE + _debug2 DEPLOY_LIGHTTPD_BUNDLE "${DEPLOY_LIGHTTPD_BUNDLE}" + if [ -n "${DEPLOY_LIGHTTPD_BUNDLE}" ]; then + Le_Deploy_lighttpd_bundle="${DEPLOY_LIGHTTPD_BUNDLE}" + _savedomainconf Le_Deploy_lighttpd_bundle "${Le_Deploy_lighttpd_bundle}" + elif [ -z "${Le_Deploy_lighttpd_bundle}" ]; then + Le_Deploy_lighttpd_bundle="${DEPLOY_LIGHTTPD_BUNDLE_DEFAULT}" + fi + + # ISSUER is optional. If not provided then assume "${DEPLOY_LIGHTTPD_ISSUER_DEFAULT}" + _getdeployconf DEPLOY_LIGHTTPD_ISSUER + _debug2 DEPLOY_LIGHTTPD_ISSUER "${DEPLOY_LIGHTTPD_ISSUER}" + if [ -n "${DEPLOY_LIGHTTPD_ISSUER}" ]; then + Le_Deploy_lighttpd_issuer="${DEPLOY_LIGHTTPD_ISSUER}" + _savedomainconf Le_Deploy_lighttpd_issuer "${Le_Deploy_lighttpd_issuer}" + elif [ -z "${Le_Deploy_lighttpd_issuer}" ]; then + Le_Deploy_lighttpd_issuer="${DEPLOY_LIGHTTPD_ISSUER_DEFAULT}" + fi + + # RELOAD is optional. If not provided then assume "${DEPLOY_LIGHTTPD_RELOAD_DEFAULT}" + _getdeployconf DEPLOY_LIGHTTPD_RELOAD + _debug2 DEPLOY_LIGHTTPD_RELOAD "${DEPLOY_LIGHTTPD_RELOAD}" + if [ -n "${DEPLOY_LIGHTTPD_RELOAD}" ]; then + Le_Deploy_lighttpd_reload="${DEPLOY_LIGHTTPD_RELOAD}" + _savedomainconf Le_Deploy_lighttpd_reload "${Le_Deploy_lighttpd_reload}" + elif [ -z "${Le_Deploy_lighttpd_reload}" ]; then + Le_Deploy_lighttpd_reload="${DEPLOY_LIGHTTPD_RELOAD_DEFAULT}" + fi + + # Set the suffix depending if we are creating a bundle or not + if [ "${Le_Deploy_lighttpd_bundle}" = "yes" ]; then + _info "Bundle creation requested" + # Initialise $Le_Keylength if its not already set + if [ -z "${Le_Keylength}" ]; then + Le_Keylength="" + fi + if _isEccKey "${Le_Keylength}"; then + _info "ECC key type detected" + _suffix=".ecdsa" + else + _info "RSA key type detected" + _suffix=".rsa" + fi + else + _suffix="" + fi + _debug _suffix "${_suffix}" + + # Set variables for later + _pem="${Le_Deploy_lighttpd_pem_path}/${Le_Deploy_lighttpd_pem_name}${_suffix}" + _issuer="${_pem}.issuer" + _ocsp="${_pem}.ocsp" + _reload="${Le_Deploy_lighttpd_reload}" + + _info "Deploying PEM file" + # Create a temporary PEM file + _temppem="$(_mktemp)" + _debug _temppem "${_temppem}" + cat "${_ckey}" "${_ccert}" "${_cca}" >"${_temppem}" + _ret="$?" + + # Check that we could create the temporary file + if [ "${_ret}" != "0" ]; then + _err "Error code ${_ret} returned during PEM file creation" + [ -f "${_temppem}" ] && rm -f "${_temppem}" + return ${_ret} + fi + + # Move PEM file into place + _info "Moving new certificate into place" + _debug _pem "${_pem}" + cat "${_temppem}" >"${_pem}" + _ret=$? + + # Clean up temp file + [ -f "${_temppem}" ] && rm -f "${_temppem}" + + # Deal with any failure of moving PEM file into place + if [ "${_ret}" != "0" ]; then + _err "Error code ${_ret} returned while moving new certificate into place" + return ${_ret} + fi + + # Update .issuer file if requested + if [ "${Le_Deploy_lighttpd_issuer}" = "yes" ]; then + _info "Updating .issuer file" + _debug _issuer "${_issuer}" + cat "${_cca}" >"${_issuer}" + _ret="$?" + + if [ "${_ret}" != "0" ]; then + _err "Error code ${_ret} returned while copying issuer/CA certificate into place" + return ${_ret} + fi + else + [ -f "${_issuer}" ] && _err "Issuer file update not requested but .issuer file exists" + fi + + # Update .ocsp file if certificate was requested with --ocsp/--ocsp-must-staple option + if [ -z "${Le_OCSP_Staple}" ]; then + Le_OCSP_Staple="0" + fi + if [ "${Le_OCSP_Staple}" = "1" ]; then + _info "Updating OCSP stapling info" + _debug _ocsp "${_ocsp}" + _info "Extracting OCSP URL" + _ocsp_url=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -ocsp_uri -in "${_pem}") + _debug _ocsp_url "${_ocsp_url}" + + # Only process OCSP if URL was present + if [ "${_ocsp_url}" != "" ]; then + # Extract the hostname from the OCSP URL + _info "Extracting OCSP URL" + _ocsp_host=$(echo "${_ocsp_url}" | cut -d/ -f3) + _debug _ocsp_host "${_ocsp_host}" + + # Only process the certificate if we have a .issuer file + if [ -r "${_issuer}" ]; then + # Check if issuer cert is also a root CA cert + _subjectdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10) + _debug _subjectdn "${_subjectdn}" + _issuerdn=$(${ACME_OPENSSL_BIN:-openssl} x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10) + _debug _issuerdn "${_issuerdn}" + _info "Requesting OCSP response" + # If the issuer is a CA cert then our command line has "-CAfile" added + if [ "${_subjectdn}" = "${_issuerdn}" ]; then + _cafile_argument="-CAfile \"${_issuer}\"" + else + _cafile_argument="" + fi + _debug _cafile_argument "${_cafile_argument}" + # if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed + _openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2) + _debug _openssl_version "${_openssl_version}" + _openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1) + _openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2) + if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then + _header_sep="=" + else + _header_sep=" " + fi + # Request the OCSP response from the issuer and store it + _openssl_ocsp_cmd="${ACME_OPENSSL_BIN:-openssl} ocsp \ + -issuer \"${_issuer}\" \ + -cert \"${_pem}\" \ + -url \"${_ocsp_url}\" \ + -header Host${_header_sep}\"${_ocsp_host}\" \ + -respout \"${_ocsp}\" \ + -verify_other \"${_issuer}\" \ + ${_cafile_argument} \ + | grep -q \"${_pem}: good\"" + _debug _openssl_ocsp_cmd "${_openssl_ocsp_cmd}" + eval "${_openssl_ocsp_cmd}" + _ret=$? + else + # Non fatal: No issuer file was present so no OCSP stapling file created + _err "OCSP stapling in use but no .issuer file was present" + fi + else + # Non fatal: No OCSP url was found int the certificate + _err "OCSP update requested but no OCSP URL was found in certificate" + fi + + # Non fatal: Check return code of openssl command + if [ "${_ret}" != "0" ]; then + _err "Updating OCSP stapling failed with return code ${_ret}" + fi + else + # An OCSP file was already present but certificate did not have OCSP extension + if [ -f "${_ocsp}" ]; then + _err "OCSP was not requested but .ocsp file exists." + # Could remove the file at this step, although Lighttpd just ignores it in this case + # rm -f "${_ocsp}" || _err "Problem removing stale .ocsp file" + fi + fi + + # Reload Lighttpd + _debug _reload "${_reload}" + eval "${_reload}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Error code ${_ret} during reload" + return ${_ret} + else + _info "Reload successful" + fi + + return 0 +} diff --git a/deploy/ssh.sh b/deploy/ssh.sh index 18de4aa6..89962621 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -35,11 +35,6 @@ ssh_deploy() { _cfullchain="$5" _deploy_ssh_servers="" - if [ -f "$DOMAIN_CONF" ]; then - # shellcheck disable=SC1090 - . "$DOMAIN_CONF" - fi - _debug _cdomain "$_cdomain" _debug _ckey "$_ckey" _debug _ccert "$_ccert" @@ -47,6 +42,8 @@ ssh_deploy() { _debug _cfullchain "$_cfullchain" # USER is required to login by SSH to remote host. + _getdeployconf DEPLOY_SSH_USER + _debug2 DEPLOY_SSH_USER "$DEPLOY_SSH_USER" if [ -z "$DEPLOY_SSH_USER" ]; then if [ -z "$Le_Deploy_ssh_user" ]; then _err "DEPLOY_SSH_USER not defined." @@ -58,6 +55,8 @@ ssh_deploy() { fi # SERVER is optional. If not provided then use _cdomain + _getdeployconf DEPLOY_SSH_SERVER + _debug2 DEPLOY_SSH_SERVER "$DEPLOY_SSH_SERVER" if [ -n "$DEPLOY_SSH_SERVER" ]; then Le_Deploy_ssh_server="$DEPLOY_SSH_SERVER" _savedomainconf Le_Deploy_ssh_server "$Le_Deploy_ssh_server" @@ -66,6 +65,8 @@ ssh_deploy() { fi # CMD is optional. If not provided then use ssh + _getdeployconf DEPLOY_SSH_CMD + _debug2 DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD" if [ -n "$DEPLOY_SSH_CMD" ]; then Le_Deploy_ssh_cmd="$DEPLOY_SSH_CMD" _savedomainconf Le_Deploy_ssh_cmd "$Le_Deploy_ssh_cmd" @@ -74,6 +75,8 @@ ssh_deploy() { fi # BACKUP is optional. If not provided then default to previously saved value or yes. + _getdeployconf DEPLOY_SSH_BACKUP + _debug2 DEPLOY_SSH_BACKUP "$DEPLOY_SSH_BACKUP" if [ "$DEPLOY_SSH_BACKUP" = "no" ]; then Le_Deploy_ssh_backup="no" elif [ -z "$Le_Deploy_ssh_backup" ] || [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then @@ -82,6 +85,8 @@ ssh_deploy() { _savedomainconf Le_Deploy_ssh_backup "$Le_Deploy_ssh_backup" # BACKUP_PATH is optional. If not provided then default to previously saved value or .acme_ssh_deploy + _getdeployconf DEPLOY_SSH_BACKUP_PATH + _debug2 DEPLOY_SSH_BACKUP_PATH "$DEPLOY_SSH_BACKUP_PATH" if [ -n "$DEPLOY_SSH_BACKUP_PATH" ]; then Le_Deploy_ssh_backup_path="$DEPLOY_SSH_BACKUP_PATH" elif [ -z "$Le_Deploy_ssh_backup_path" ]; then @@ -91,6 +96,8 @@ ssh_deploy() { # MULTI_CALL is optional. If not provided then default to previously saved # value (which may be undefined... equivalent to "no"). + _getdeployconf DEPLOY_SSH_MULTI_CALL + _debug2 DEPLOY_SSH_MULTI_CALL "$DEPLOY_SSH_MULTI_CALL" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then Le_Deploy_ssh_multi_call="yes" _savedomainconf Le_Deploy_ssh_multi_call "$Le_Deploy_ssh_multi_call" @@ -141,6 +148,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d # KEYFILE is optional. # If provided then private key will be copied to provided filename. + _getdeployconf DEPLOY_SSH_KEYFILE + _debug2 DEPLOY_SSH_KEYFILE "$DEPLOY_SSH_KEYFILE" if [ -n "$DEPLOY_SSH_KEYFILE" ]; then Le_Deploy_ssh_keyfile="$DEPLOY_SSH_KEYFILE" _savedomainconf Le_Deploy_ssh_keyfile "$Le_Deploy_ssh_keyfile" @@ -163,6 +172,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d # CERTFILE is optional. # If provided then certificate will be copied or appended to provided filename. + _getdeployconf DEPLOY_SSH_CERTFILE + _debug2 DEPLOY_SSH_CERTFILE "$DEPLOY_SSH_CERTFILE" if [ -n "$DEPLOY_SSH_CERTFILE" ]; then Le_Deploy_ssh_certfile="$DEPLOY_SSH_CERTFILE" _savedomainconf Le_Deploy_ssh_certfile "$Le_Deploy_ssh_certfile" @@ -189,6 +200,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d # CAFILE is optional. # If provided then CA intermediate certificate will be copied or appended to provided filename. + _getdeployconf DEPLOY_SSH_CAFILE + _debug2 DEPLOY_SSH_CAFILE "$DEPLOY_SSH_CAFILE" if [ -n "$DEPLOY_SSH_CAFILE" ]; then Le_Deploy_ssh_cafile="$DEPLOY_SSH_CAFILE" _savedomainconf Le_Deploy_ssh_cafile "$Le_Deploy_ssh_cafile" @@ -216,6 +229,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d # FULLCHAIN is optional. # If provided then fullchain certificate will be copied or appended to provided filename. + _getdeployconf DEPLOY_SSH_FULLCHAIN + _debug2 DEPLOY_SSH_FULLCHAIN "$DEPLOY_SSH_FULLCHAIN" if [ -n "$DEPLOY_SSH_FULLCHAIN" ]; then Le_Deploy_ssh_fullchain="$DEPLOY_SSH_FULLCHAIN" _savedomainconf Le_Deploy_ssh_fullchain "$Le_Deploy_ssh_fullchain" @@ -244,6 +259,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d # REMOTE_CMD is optional. # If provided then this command will be executed on remote host. + _getdeployconf DEPLOY_SSH_REMOTE_CMD + _debug2 DEPLOY_SSH_REMOTE_CMD "$DEPLOY_SSH_REMOTE_CMD" if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then Le_Deploy_ssh_remote_cmd="$DEPLOY_SSH_REMOTE_CMD" _savedomainconf Le_Deploy_ssh_remote_cmd "$Le_Deploy_ssh_remote_cmd" diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index 35d33209..177b3fbe 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -66,6 +66,12 @@ synology_dsm_deploy() { _getdeployconf SYNO_Certificate _debug SYNO_Certificate "${SYNO_Certificate:-}" + # shellcheck disable=SC1003 # We are not trying to escape a single quote + if printf "%s" "$SYNO_Certificate" | grep '\\'; then + _err "Do not use a backslash (\) in your certificate description" + return 1 + fi + _base_url="$SYNO_Scheme://$SYNO_Hostname:$SYNO_Port" _debug _base_url "$_base_url" @@ -94,6 +100,7 @@ synology_dsm_deploy() { if [ -z "$token" ]; then _err "Unable to authenticate to $SYNO_Hostname:$SYNO_Port using $SYNO_Scheme." _err "Check your username and password." + _err "If two-factor authentication is enabled for the user, you have to choose another user." return 1 fi sid=$(echo "$response" | grep "sid" | sed -n 's/.*"sid" *: *"\([^"]*\).*/\1/p') @@ -110,7 +117,9 @@ synology_dsm_deploy() { _info "Getting certificates in Synology DSM" response=$(_post "api=SYNO.Core.Certificate.CRT&method=list&version=1&_sid=$sid" "$_base_url/webapi/entry.cgi") _debug3 response "$response" - id=$(echo "$response" | sed -n "s/.*\"desc\":\"$SYNO_Certificate\",\"id\":\"\([^\"]*\).*/\1/p") + escaped_certificate="$(printf "%s" "$SYNO_Certificate" | sed 's/\([].*^$[]\)/\\\1/g;s/"/\\\\"/g')" + _debug escaped_certificate "$escaped_certificate" + id=$(echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\"id\":\"\([^\"]*\).*/\1/p") _debug2 id "$id" if [ -z "$id" ] && [ -z "${SYNO_Create:-}" ]; then @@ -119,13 +128,7 @@ synology_dsm_deploy() { fi # we've verified this certificate description is a thing, so save it - _savedeployconf SYNO_Certificate "$SYNO_Certificate" - - default=false - if echo "$response" | sed -n "s/.*\"desc\":\"$SYNO_Certificate\",\([^{]*\).*/\1/p" | grep -- 'is_default":true' >/dev/null; then - default=true - fi - _debug2 default "$default" + _savedeployconf SYNO_Certificate "$SYNO_Certificate" "base64" _info "Generate form POST request" nl="\0015\0012" @@ -135,7 +138,12 @@ synology_dsm_deploy() { content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"inter_cert\"; filename=\"$(basename "$_cca")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cca")\0012" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"id\"${nl}${nl}$id" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"desc\"${nl}${nl}${SYNO_Certificate}" - content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"as_default\"${nl}${nl}${default}" + if echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\([^{]*\).*/\1/p" | grep -- 'is_default":true' >/dev/null; then + _debug2 default "this is the default certificate" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"as_default\"${nl}${nl}true" + else + _debug2 default "this is NOT the default certificate" + fi content="$content${nl}--$delim--${nl}" content="$(printf "%b_" "$content")" content="${content%_}" # protect trailing \n diff --git a/deploy/unifi.sh b/deploy/unifi.sh index 184aa62e..a864135e 100644 --- a/deploy/unifi.sh +++ b/deploy/unifi.sh @@ -1,12 +1,43 @@ #!/usr/bin/env sh -#Here is a script to deploy cert to unifi server. +# Here is a script to deploy cert on a Unifi Controller or Cloud Key device. +# It supports: +# - self-hosted Unifi Controller +# - Unifi Cloud Key (Gen1/2/2+) +# - Unifi Cloud Key running UnifiOS (v2.0.0+, Gen2/2+ only) +# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/3359 #returns 0 means success, otherwise error. +# The deploy-hook automatically detects standard Unifi installations +# for each of the supported environments. Most users should not need +# to set any of these variables, but if you are running a self-hosted +# Controller with custom locations, set these as necessary before running +# the deploy hook. (Defaults shown below.) +# +# Settings for Unifi Controller: +# Location of Java keystore or unifi.keystore.jks file: #DEPLOY_UNIFI_KEYSTORE="/usr/lib/unifi/data/keystore" +# Keystore password (built into Unifi Controller, not a user-set password): #DEPLOY_UNIFI_KEYPASS="aircontrolenterprise" +# Command to restart Unifi Controller: #DEPLOY_UNIFI_RELOAD="service unifi restart" +# +# Settings for Unifi Cloud Key Gen1 (nginx admin pages): +# Directory where cloudkey.crt and cloudkey.key live: +#DEPLOY_UNIFI_CLOUDKEY_CERTDIR="/etc/ssl/private" +# Command to restart maintenance pages and Controller +# (same setting as above, default is updated when running on Cloud Key Gen1): +#DEPLOY_UNIFI_RELOAD="service nginx restart && service unifi restart" +# +# Settings for UnifiOS (Cloud Key Gen2): +# Directory where unifi-core.crt and unifi-core.key live: +#DEPLOY_UNIFI_CORE_CONFIG="/data/unifi-core/config/" +# Command to restart unifi-core: +#DEPLOY_UNIFI_RELOAD="systemctl restart unifi-core" +# +# At least one of DEPLOY_UNIFI_KEYSTORE, DEPLOY_UNIFI_CLOUDKEY_CERTDIR, +# or DEPLOY_UNIFI_CORE_CONFIG must exist to receive the deployed certs. ######## Public functions ##################### @@ -24,77 +55,160 @@ unifi_deploy() { _debug _cca "$_cca" _debug _cfullchain "$_cfullchain" - if ! _exists keytool; then - _err "keytool not found" - return 1 - fi + _getdeployconf DEPLOY_UNIFI_KEYSTORE + _getdeployconf DEPLOY_UNIFI_KEYPASS + _getdeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR + _getdeployconf DEPLOY_UNIFI_CORE_CONFIG + _getdeployconf DEPLOY_UNIFI_RELOAD - DEFAULT_UNIFI_KEYSTORE="/usr/lib/unifi/data/keystore" - _unifi_keystore="${DEPLOY_UNIFI_KEYSTORE:-$DEFAULT_UNIFI_KEYSTORE}" - DEFAULT_UNIFI_KEYPASS="aircontrolenterprise" - _unifi_keypass="${DEPLOY_UNIFI_KEYPASS:-$DEFAULT_UNIFI_KEYPASS}" - DEFAULT_UNIFI_RELOAD="service unifi restart" - _reload="${DEPLOY_UNIFI_RELOAD:-$DEFAULT_UNIFI_RELOAD}" + _debug2 DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE" + _debug2 DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS" + _debug2 DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR" + _debug2 DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG" + _debug2 DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD" - _debug _unifi_keystore "$_unifi_keystore" - if [ ! -f "$_unifi_keystore" ]; then - if [ -z "$DEPLOY_UNIFI_KEYSTORE" ]; then - _err "unifi keystore is not found, please define DEPLOY_UNIFI_KEYSTORE" - return 1 - else - _err "It seems that the specified unifi keystore is not valid, please check." + # Space-separated list of environments detected and installed: + _services_updated="" + + # Default reload commands accumulated as we auto-detect environments: + _reload_cmd="" + + # Unifi Controller environment (self hosted or any Cloud Key) -- + # auto-detect by file /usr/lib/unifi/data/keystore: + _unifi_keystore="${DEPLOY_UNIFI_KEYSTORE:-/usr/lib/unifi/data/keystore}" + if [ -f "$_unifi_keystore" ]; then + _info "Installing certificate for Unifi Controller (Java keystore)" + _debug _unifi_keystore "$_unifi_keystore" + if ! _exists keytool; then + _err "keytool not found" return 1 fi - fi - if [ ! -w "$_unifi_keystore" ]; then - _err "The file $_unifi_keystore is not writable, please change the permission." + if [ ! -w "$_unifi_keystore" ]; then + _err "The file $_unifi_keystore is not writable, please change the permission." + return 1 + fi + + _unifi_keypass="${DEPLOY_UNIFI_KEYPASS:-aircontrolenterprise}" + + _debug "Generate import pkcs12" + _import_pkcs12="$(_mktemp)" + _toPkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca" "$_unifi_keypass" unifi root + # shellcheck disable=SC2181 + if [ "$?" != "0" ]; then + _err "Error generating pkcs12. Please re-run with --debug and report a bug." + return 1 + fi + + _debug "Import into keystore: $_unifi_keystore" + if keytool -importkeystore \ + -deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \ + -srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \ + -alias unifi -noprompt; then + _debug "Import keystore success!" + rm "$_import_pkcs12" + else + _err "Error importing into Unifi Java keystore." + _err "Please re-run with --debug and report a bug." + rm "$_import_pkcs12" + return 1 + fi + + if systemctl -q is-active unifi; then + _reload_cmd="${_reload_cmd:+$_reload_cmd && }service unifi restart" + fi + _services_updated="${_services_updated} unifi" + _info "Install Unifi Controller certificate success!" + elif [ "$DEPLOY_UNIFI_KEYSTORE" ]; then + _err "The specified DEPLOY_UNIFI_KEYSTORE='$DEPLOY_UNIFI_KEYSTORE' is not valid, please check." return 1 fi - _info "Generate import pkcs12" - _import_pkcs12="$(_mktemp)" - _toPkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca" "$_unifi_keypass" unifi root - if [ "$?" != "0" ]; then - _err "Oops, error creating import pkcs12, please report bug to us." + # Cloud Key environment (non-UnifiOS -- nginx serves admin pages) -- + # auto-detect by file /etc/ssl/private/cloudkey.key: + _cloudkey_certdir="${DEPLOY_UNIFI_CLOUDKEY_CERTDIR:-/etc/ssl/private}" + if [ -f "${_cloudkey_certdir}/cloudkey.key" ]; then + _info "Installing certificate for Cloud Key Gen1 (nginx admin pages)" + _debug _cloudkey_certdir "$_cloudkey_certdir" + if [ ! -w "$_cloudkey_certdir" ]; then + _err "The directory $_cloudkey_certdir is not writable; please check permissions." + return 1 + fi + # Cloud Key expects to load the keystore from /etc/ssl/private/unifi.keystore.jks. + # Normally /usr/lib/unifi/data/keystore is a symlink there (so the keystore was + # updated above), but if not, we don't know how to handle this installation: + if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then + _err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'" + return 1 + fi + + cat "$_cfullchain" >"${_cloudkey_certdir}/cloudkey.crt" + cat "$_ckey" >"${_cloudkey_certdir}/cloudkey.key" + (cd "$_cloudkey_certdir" && tar -cf cert.tar cloudkey.crt cloudkey.key unifi.keystore.jks) + + if systemctl -q is-active nginx; then + _reload_cmd="${_reload_cmd:+$_reload_cmd && }service nginx restart" + fi + _info "Install Cloud Key Gen1 certificate success!" + _services_updated="${_services_updated} nginx" + elif [ "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR" ]; then + _err "The specified DEPLOY_UNIFI_CLOUDKEY_CERTDIR='$DEPLOY_UNIFI_CLOUDKEY_CERTDIR' is not valid, please check." return 1 fi - _info "Modify unifi keystore: $_unifi_keystore" - if keytool -importkeystore \ - -deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \ - -srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \ - -alias unifi -noprompt; then - _info "Import keystore success!" - rm "$_import_pkcs12" - else - _err "Import unifi keystore error, please report bug to us." - rm "$_import_pkcs12" + # UnifiOS environment -- auto-detect by /data/unifi-core/config/unifi-core.key: + _unifi_core_config="${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}" + if [ -f "${_unifi_core_config}/unifi-core.key" ]; then + _info "Installing certificate for UnifiOS" + _debug _unifi_core_config "$_unifi_core_config" + if [ ! -w "$_unifi_core_config" ]; then + _err "The directory $_unifi_core_config is not writable; please check permissions." + return 1 + fi + + cat "$_cfullchain" >"${_unifi_core_config}/unifi-core.crt" + cat "$_ckey" >"${_unifi_core_config}/unifi-core.key" + + if systemctl -q is-active unifi-core; then + _reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl restart unifi-core" + fi + _info "Install UnifiOS certificate success!" + _services_updated="${_services_updated} unifi-core" + elif [ "$DEPLOY_UNIFI_CORE_CONFIG" ]; then + _err "The specified DEPLOY_UNIFI_CORE_CONFIG='$DEPLOY_UNIFI_CORE_CONFIG' is not valid, please check." return 1 fi - _info "Run reload: $_reload" - if eval "$_reload"; then + if [ -z "$_services_updated" ]; then + # None of the Unifi environments were auto-detected, so no deployment has occurred + # (and none of DEPLOY_UNIFI_{KEYSTORE,CLOUDKEY_CERTDIR,CORE_CONFIG} were set). + _err "Unable to detect Unifi environment in standard location." + _err "(This deploy hook must be run on the Unifi device, not a remote machine.)" + _err "For non-standard Unifi installations, set DEPLOY_UNIFI_KEYSTORE," + _err "DEPLOY_UNIFI_CLOUDKEY_CERTDIR, and/or DEPLOY_UNIFI_CORE_CONFIG as appropriate." + return 1 + fi + + _reload_cmd="${DEPLOY_UNIFI_RELOAD:-$_reload_cmd}" + if [ -z "$_reload_cmd" ]; then + _err "Certificates were installed for services:${_services_updated}," + _err "but none appear to be active. Please set DEPLOY_UNIFI_RELOAD" + _err "to a command that will restart the necessary services." + return 1 + fi + _info "Reload services (this may take some time): $_reload_cmd" + if eval "$_reload_cmd"; then _info "Reload success!" - if [ "$DEPLOY_UNIFI_KEYSTORE" ]; then - _savedomainconf DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE" - else - _cleardomainconf DEPLOY_UNIFI_KEYSTORE - fi - if [ "$DEPLOY_UNIFI_KEYPASS" ]; then - _savedomainconf DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS" - else - _cleardomainconf DEPLOY_UNIFI_KEYPASS - fi - if [ "$DEPLOY_UNIFI_RELOAD" ]; then - _savedomainconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD" - else - _cleardomainconf DEPLOY_UNIFI_RELOAD - fi - return 0 else _err "Reload error" return 1 fi - return 0 + # Successful, so save all (non-default) config: + _savedeployconf DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE" + _savedeployconf DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS" + _savedeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR" + _savedeployconf DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG" + _savedeployconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD" + + return 0 } diff --git a/deploy/vault_cli.sh b/deploy/vault_cli.sh index 8b854137..cbb8cc59 100644 --- a/deploy/vault_cli.sh +++ b/deploy/vault_cli.sh @@ -50,12 +50,12 @@ vault_cli_deploy() { fi if [ -n "$FABIO" ]; then - $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}" cert=@"$_cfullchain" key=@"$_ckey" || return 1 + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}" cert=@"$_cfullchain" key=@"$_ckey" || return 1 else - $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1 - $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1 - $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1 - $VAULT_CMD write "${VAULT_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1 + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1 + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1 + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1 + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1 fi } diff --git a/dnsapi/dns_1984hosting.sh b/dnsapi/dns_1984hosting.sh index d720c1c5..db0cbe15 100755 --- a/dnsapi/dns_1984hosting.sh +++ b/dnsapi/dns_1984hosting.sh @@ -46,7 +46,7 @@ dns_1984hosting_add() { postdata="entry=new" postdata="$postdata&type=TXT" - postdata="$postdata&ttl=3600" + postdata="$postdata&ttl=900" postdata="$postdata&zone=$_domain" postdata="$postdata&host=$_sub_domain" postdata="$postdata&rdata=%22$value%22" @@ -59,7 +59,7 @@ dns_1984hosting_add() { if _contains "$response" '"haserrors": true'; then _err "1984Hosting failed to add TXT record for $_sub_domain bad RC from _post" return 1 - elif _contains "$response" ""; then + elif _contains "$response" "html>"; then _err "1984Hosting failed to add TXT record for $_sub_domain. Check $HTTP_HEADER file" return 1 elif _contains "$response" '"auth": false'; then @@ -93,20 +93,15 @@ dns_1984hosting_rm() { fi _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - _debug "Delete $fulldomain TXT record" - url="https://management.1984hosting.com/domains" - _htmlget "$url" "$_domain" - _debug2 _response "$_response" - zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+')" - _debug2 zone_id "$zone_id" - if [ -z "$zone_id" ]; then - _err "Error getting zone_id for $1" + url="https://management.1984hosting.com/domains" + if ! _get_zone_id "$url" "$_domain"; then + _err "invalid zone" "$_domain" return 1 fi - _htmlget "$url/$zone_id" "$_sub_domain" + _htmlget "$url/$_zone_id" "$txtvalue" _debug2 _response "$_response" entry_id="$(echo "$_response" | _egrep_o 'entry_[0-9]+' | sed 's/entry_//')" _debug2 entry_id "$entry_id" @@ -135,7 +130,7 @@ dns_1984hosting_rm() { _1984hosting_login() { if ! _check_credentials; then return 1; fi - if _check_cookie; then + if _check_cookies; then _debug "Already logged in" return 0 fi @@ -145,14 +140,17 @@ _1984hosting_login() { password=$(printf '%s' "$One984HOSTING_Password" | _url_encode) url="https://management.1984hosting.com/accounts/checkuserauth/" - response="$(_post "username=$username&password=$password&otpkey=" "$url")" + response="$(_post "username=$username&password=$password&otpkey=" $url)" response="$(echo "$response" | _normalizeJson)" _debug2 response "$response" if _contains "$response" '"loggedin": true'; then - One984HOSTING_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _tail_n 1 | _egrep_o 'sessionid=[^;]*;' | tr -d ';')" - export One984HOSTING_COOKIE - _saveaccountconf_mutable One984HOSTING_COOKIE "$One984HOSTING_COOKIE" + One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')" + One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')" + export One984HOSTING_SESSIONID_COOKIE + export One984HOSTING_CSRFTOKEN_COOKIE + _saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE" + _saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE" return 0 fi return 1 @@ -169,22 +167,24 @@ _check_credentials() { return 0 } -_check_cookie() { - One984HOSTING_COOKIE="${One984HOSTING_COOKIE:-$(_readaccountconf_mutable One984HOSTING_COOKIE)}" - if [ -z "$One984HOSTING_COOKIE" ]; then - _debug "No cached cookie found" +_check_cookies() { + One984HOSTING_SESSIONID_COOKIE="${One984HOSTING_SESSIONID_COOKIE:-$(_readaccountconf_mutable One984HOSTING_SESSIONID_COOKIE)}" + One984HOSTING_CSRFTOKEN_COOKIE="${One984HOSTING_CSRFTOKEN_COOKIE:-$(_readaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE)}" + if [ -z "$One984HOSTING_SESSIONID_COOKIE" ] || [ -z "$One984HOSTING_CSRFTOKEN_COOKIE" ]; then + _debug "No cached cookie(s) found" return 1 fi _authget "https://management.1984hosting.com/accounts/loginstatus/" - response="$(echo "$_response" | _normalizeJson)" if _contains "$response" '"ok": true'; then - _debug "Cached cookie still valid" + _debug "Cached cookies still valid" return 0 fi - _debug "Cached cookie no longer valid" - One984HOSTING_COOKIE="" - _saveaccountconf_mutable One984HOSTING_COOKIE "$One984HOSTING_COOKIE" + _debug "Cached cookies no longer valid" + One984HOSTING_SESSIONID_COOKIE="" + One984HOSTING_CSRFTOKEN_COOKIE="" + _saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE" + _saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE" return 1 } @@ -194,7 +194,7 @@ _check_cookie() { # _domain=domain.com _get_root() { domain="$1" - i=2 + i=1 p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f $i-100) @@ -205,7 +205,7 @@ _get_root() { fi _authget "https://management.1984hosting.com/domains/soacheck/?zone=$h&nameserver=ns0.1984.is." - if _contains "$_response" "serial"; then + if _contains "$_response" "serial" && ! _contains "$_response" "null"; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) _domain="$h" return 0 @@ -216,21 +216,46 @@ _get_root() { return 1 } +#usage: _get_zone_id url domain.com +#returns zone id for domain.com +_get_zone_id() { + url=$1 + domain=$2 + _htmlget "$url" "$domain" + _debug2 _response "$_response" + _zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+' | _head_n 1)" + _debug2 _zone_id "$_zone_id" + if [ -z "$_zone_id" ]; then + _err "Error getting _zone_id for $2" + return 1 + fi + return 0 +} + # add extra headers to request _authget() { - export _H1="Cookie: $One984HOSTING_COOKIE" - _response=$(_get "$1") + export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE;$One984HOSTING_SESSIONID_COOKIE" + _response=$(_get "$1" | _normalizeJson) + _debug2 _response "$_response" } # truncate huge HTML response # echo: Argument list too long _htmlget() { - export _H1="Cookie: $One984HOSTING_COOKIE" - _response=$(_get "$1" | grep "$2" | _head_n 1) + export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE;$One984HOSTING_SESSIONID_COOKIE" + _response=$(_get "$1" | grep "$2") + if _contains "$_response" "@$2"; then + _response=$(echo "$_response" | grep -v "[@]" | _head_n 1) + fi } # add extra headers to request _authpost() { - export _H1="Cookie: $One984HOSTING_COOKIE" + url="https://management.1984hosting.com/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://management.1984hosting.com/domains/$_zone_id" + export _H3="X-CSRFToken: $csrf_header" _response=$(_post "$1" "$2") } diff --git a/dnsapi/dns_arvan.sh b/dnsapi/dns_arvan.sh index ca1f56c7..4c9217e5 100644 --- a/dnsapi/dns_arvan.sh +++ b/dnsapi/dns_arvan.sh @@ -1,10 +1,9 @@ #!/usr/bin/env sh -#Arvan_Token="xxxx" +#Arvan_Token="Apikey xxxx" ARVAN_API_URL="https://napi.arvancloud.com/cdn/4.0/domains" - -#Author: Ehsan Aliakbar +#Author: Vahid Fardi #Report Bugs here: https://github.com/Neilpang/acme.sh # ######## Public functions ##################### @@ -38,6 +37,7 @@ dns_arvan_add() { _info "Adding record" if _arvan_rest POST "$_domain/dns-records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":{\"text\":\"$txtvalue\"},\"ttl\":120}"; then if _contains "$response" "$txtvalue"; then + _info "response id is $response" _info "Added, OK" return 0 elif _contains "$response" "Record Data is Duplicated"; then @@ -49,7 +49,7 @@ dns_arvan_add() { fi fi _err "Add txt record error." - return 1 + return 0 } #Usage: fulldomain txtvalue @@ -73,33 +73,21 @@ dns_arvan_rm() { _debug _domain "$_domain" _debug "Getting txt records" - shorted_txtvalue=$(printf "%s" "$txtvalue" | cut -d "-" -d "_" -f1) - _arvan_rest GET "${_domain}/dns-records?search=$shorted_txtvalue" - + _arvan_rest GET "${_domain}/dns-records" if ! printf "%s" "$response" | grep \"current_page\":1 >/dev/null; then _err "Error on Arvan Api" _err "Please create a github issue with debbug log" return 1 fi - count=$(printf "%s\n" "$response" | _egrep_o "\"total\":[^,]*" | cut -d : -f 2) - _debug count "$count" - if [ "$count" = "0" ]; then - _info "Don't need to remove." - else - record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1) - _debug "record_id" "$record_id" - if [ -z "$record_id" ]; then - _err "Can not get record id to remove." - return 1 - fi - if ! _arvan_rest "DELETE" "${_domain}/dns-records/$record_id"; then - _err "Delete record error." - return 1 - fi - _debug "$response" - _contains "$response" 'dns record deleted' + _record_id=$(echo "$response" | _egrep_o ".\"id\":\"[^\"]*\",\"type\":\"txt\",\"name\":\"_acme-challenge\",\"value\":{\"text\":\"$txtvalue\"}" | cut -d : -f 2 | cut -d , -f 1 | tr -d \") + if ! _arvan_rest "DELETE" "${_domain}/dns-records/${_record_id}"; then + _err "Error on Arvan Api" + return 1 fi + _debug "$response" + _contains "$response" 'dns record deleted' + return 0 } #################### Private functions below ################################## @@ -111,7 +99,7 @@ dns_arvan_rm() { # _domain_id=sdjkglgdfewsdfg _get_root() { domain=$1 - i=1 + i=2 p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f $i-100) @@ -121,12 +109,11 @@ _get_root() { return 1 fi - if ! _arvan_rest GET "?search=$h"; then + if ! _arvan_rest GET "$h"; then return 1 fi - - if _contains "$response" "\"domain\":\"$h\"" || _contains "$response" '"total":1'; then - _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + if _contains "$response" "\"domain\":\"$h\""; then + _domain_id=$(echo "$response" | cut -d : -f 3 | cut -d , -f 1 | tr -d \") if [ "$_domain_id" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) _domain=$h @@ -146,7 +133,6 @@ _arvan_rest() { data="$3" token_trimmed=$(echo "$Arvan_Token" | tr -d '"') - export _H1="Authorization: $token_trimmed" if [ "$mtd" = "DELETE" ]; then @@ -160,4 +146,5 @@ _arvan_rest() { else response="$(_get "$ARVAN_API_URL/$ep$data")" fi + return 0 } diff --git a/dnsapi/dns_aurora.sh b/dnsapi/dns_aurora.sh new file mode 100644 index 00000000..00f44739 --- /dev/null +++ b/dnsapi/dns_aurora.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env sh + +# +#AURORA_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" +# +#AURORA_Secret="sdfsdfsdfljlbjkljlkjsdfoiwje" + +AURORA_Api="https://api.auroradns.eu" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_aurora_add() { + fulldomain=$1 + txtvalue=$2 + + AURORA_Key="${AURORA_Key:-$(_readaccountconf_mutable AURORA_Key)}" + AURORA_Secret="${AURORA_Secret:-$(_readaccountconf_mutable AURORA_Secret)}" + + if [ -z "$AURORA_Key" ] || [ -z "$AURORA_Secret" ]; then + AURORA_Key="" + AURORA_Secret="" + _err "You didn't specify an Aurora api key and secret yet." + _err "You can get yours from here https://cp.pcextreme.nl/auroradns/users." + return 1 + fi + + #save the api key and secret to the account conf file. + _saveaccountconf_mutable AURORA_Key "$AURORA_Key" + _saveaccountconf_mutable AURORA_Secret "$AURORA_Secret" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + if _aurora_rest POST "zones/$_domain_id/records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":300}"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + elif _contains "$response" "RecordExistsError"; then + _info "Already exists, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + return 1 + +} + +#fulldomain txtvalue +dns_aurora_rm() { + fulldomain=$1 + txtvalue=$2 + + AURORA_Key="${AURORA_Key:-$(_readaccountconf_mutable AURORA_Key)}" + AURORA_Secret="${AURORA_Secret:-$(_readaccountconf_mutable AURORA_Secret)}" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting records" + _aurora_rest GET "zones/${_domain_id}/records" + + if ! _contains "$response" "$txtvalue"; then + _info "Don't need to remove." + else + records=$(echo "$response" | _normalizeJson | tr -d "[]" | sed "s/},{/}|{/g" | tr "|" "\n") + if [ "$(echo "$records" | wc -l)" -le 2 ]; then + _err "Can not parse records." + return 1 + fi + record_id=$(echo "$records" | grep "\"type\": *\"TXT\"" | grep "\"name\": *\"$_sub_domain\"" | grep "\"content\": *\"$txtvalue\"" | _egrep_o "\"id\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! _aurora_rest DELETE "zones/$_domain_id/records/$record_id"; then + _err "Delete record error." + return 1 + fi + fi + return 0 + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=1 + p=1 + + 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 ! _aurora_rest GET "zones/$h"; then + return 1 + fi + + if _contains "$response" "\"name\": \"$h\""; then + _domain_id=$(echo "$response" | _normalizeJson | tr -d "{}" | tr "," "\n" | grep "\"id\": *\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") + _debug _domain_id "$_domain_id" + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_aurora_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + key_trimmed=$(echo "$AURORA_Key" | tr -d '"') + secret_trimmed=$(echo "$AURORA_Secret" | tr -d '"') + + timestamp=$(date -u +"%Y%m%dT%H%M%SZ") + signature=$(printf "%s/%s%s" "$m" "$ep" "$timestamp" | _hmac sha256 "$(printf "%s" "$secret_trimmed" | _hex_dump | tr -d " ")" | _base64) + authorization=$(printf "AuroraDNSv1 %s" "$(printf "%s:%s" "$key_trimmed" "$signature" | _base64)") + + export _H1="Content-Type: application/json; charset=UTF-8" + export _H2="X-AuroraDNS-Date: $timestamp" + export _H3="Authorization: $authorization" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$AURORA_Api/$ep" "" "$m")" + else + response="$(_get "$AURORA_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh index 068c337c..14a4594d 100755 --- a/dnsapi/dns_aws.sh +++ b/dnsapi/dns_aws.sh @@ -32,7 +32,7 @@ dns_aws_add() { if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" - _err "You haven't specifed the aws route53 api key id and and api key secret yet." + _err "You haven't specified the aws route53 api key id and and api key secret yet." _err "Please create your key and try again. see $(__green $AWS_WIKI)" return 1 fi diff --git a/dnsapi/dns_azion.sh b/dnsapi/dns_azion.sh new file mode 100644 index 00000000..f215686d --- /dev/null +++ b/dnsapi/dns_azion.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env sh + +# +#AZION_Email="" +#AZION_Password="" +# + +AZION_Api="https://api.azionapi.net" + +######## Public functions ######## + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +dns_azion_add() { + fulldomain=$1 + txtvalue=$2 + + _debug "Detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Domain not found" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug _domain_id "$_domain_id" + + _info "Add or update record" + _get_record "$_domain_id" "$_sub_domain" + if [ "$record_id" ]; then + _payload="{\"record_type\": \"TXT\", \"entry\": \"$_sub_domain\", \"answers_list\": [$answers_list, \"$txtvalue\"], \"ttl\": 20}" + if _azion_rest PUT "intelligent_dns/$_domain_id/records/$record_id" "$_payload"; then + if _contains "$response" "$txtvalue"; then + _info "Record updated." + return 0 + fi + fi + else + _payload="{\"record_type\": \"TXT\", \"entry\": \"$_sub_domain\", \"answers_list\": [\"$txtvalue\"], \"ttl\": 20}" + if _azion_rest POST "intelligent_dns/$_domain_id/records" "$_payload"; then + if _contains "$response" "$txtvalue"; then + _info "Record added." + return 0 + fi + fi + fi + _err "Failed to add or update record." + return 1 +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +dns_azion_rm() { + fulldomain=$1 + txtvalue=$2 + + _debug "Detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Domain not found" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug _domain_id "$_domain_id" + + _info "Removing record" + _get_record "$_domain_id" "$_sub_domain" + if [ "$record_id" ]; then + if _azion_rest DELETE "intelligent_dns/$_domain_id/records/$record_id"; then + _info "Record removed." + return 0 + else + _err "Failed to remove record." + return 1 + fi + else + _info "Record not found or already removed." + return 0 + fi +} + +#################### Private functions below ################################## +# Usage: _acme-challenge.www.domain.com +# returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=1 + p=1 + + if ! _azion_rest GET "intelligent_dns"; then + return 1 + fi + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + # not valid + return 1 + fi + + if _contains "$response" "\"domain\":\"$h\""; then + _domain_id=$(echo "$response" | tr '{' "\n" | grep "\"domain\":\"$h\"" | _egrep_o "\"id\":[0-9]*" | _head_n 1 | cut -d : -f 2 | tr -d \") + _debug _domain_id "$_domain_id" + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_get_record() { + _domain_id=$1 + _record=$2 + + if ! _azion_rest GET "intelligent_dns/$_domain_id/records"; then + return 1 + fi + + if _contains "$response" "\"entry\":\"$_record\""; then + _json_record=$(echo "$response" | tr '{' "\n" | grep "\"entry\":\"$_record\"") + if [ "$_json_record" ]; then + record_id=$(echo "$_json_record" | _egrep_o "\"record_id\":[0-9]*" | _head_n 1 | cut -d : -f 2 | tr -d \") + answers_list=$(echo "$_json_record" | _egrep_o "\"answers_list\":\[.*\]" | _head_n 1 | cut -d : -f 2 | tr -d \[\]) + return 0 + fi + return 1 + fi + return 1 +} + +_get_token() { + AZION_Email="${AZION_Email:-$(_readaccountconf_mutable AZION_Email)}" + AZION_Password="${AZION_Password:-$(_readaccountconf_mutable AZION_Password)}" + + if ! _contains "$AZION_Email" "@"; then + _err "It seems that the AZION_Email is not a valid email address. Revalidate your environments." + return 1 + fi + + if [ -z "$AZION_Email" ] || [ -z "$AZION_Password" ]; then + _err "You didn't specified a AZION_Email/AZION_Password to generate Azion token." + return 1 + fi + + _saveaccountconf_mutable AZION_Email "$AZION_Email" + _saveaccountconf_mutable AZION_Password "$AZION_Password" + + _basic_auth=$(printf "%s:%s" "$AZION_Email" "$AZION_Password" | _base64) + _debug _basic_auth "$_basic_auth" + + export _H1="Accept: application/json; version=3" + export _H2="Content-Type: application/json" + export _H3="Authorization: Basic $_basic_auth" + + response="$(_post "" "$AZION_Api/tokens" "" "POST")" + if _contains "$response" "\"token\":\"" >/dev/null; then + _azion_token=$(echo "$response" | _egrep_o "\"token\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \") + export AZION_Token="$_azion_token" + else + _err "Failed to generate Azion token" + return 1 + fi +} + +_azion_rest() { + _method=$1 + _uri="$2" + _data="$3" + + if [ -z "$AZION_Token" ]; then + _get_token + fi + _debug2 token "$AZION_Token" + + export _H1="Accept: application/json; version=3" + export _H2="Content-Type: application/json" + export _H3="Authorization: token $AZION_Token" + + if [ "$_method" != "GET" ]; then + _debug _data "$_data" + response="$(_post "$_data" "$AZION_Api/$_uri" "" "$_method")" + else + response="$(_get "$AZION_Api/$_uri")" + fi + + _debug2 response "$response" + + if [ "$?" != "0" ]; then + _err "error $_method $_uri $_data" + return 1 + fi + return 0 +} diff --git a/dnsapi/dns_constellix.sh b/dnsapi/dns_constellix.sh index 42df710d..69d216f0 100644 --- a/dnsapi/dns_constellix.sh +++ b/dnsapi/dns_constellix.sh @@ -30,16 +30,41 @@ dns_constellix_add() { return 1 fi - _info "Adding TXT record" - if _constellix_rest POST "domains/${_domain_id}/records" "[{\"type\":\"txt\",\"add\":true,\"set\":{\"name\":\"${_sub_domain}\",\"ttl\":120,\"roundRobin\":[{\"value\":\"${txtvalue}\"}]}}]"; then - if printf -- "%s" "$response" | grep "{\"success\":\"1 record(s) added, 0 record(s) updated, 0 record(s) deleted\"}" >/dev/null; then - _info "Added" - return 0 + # The TXT record might already exist when working with wildcard certificates. In that case, update the record by adding the new value. + _debug "Search TXT record" + if _constellix_rest GET "domains/${_domain_id}/records/TXT/search?exact=${_sub_domain}"; then + if printf -- "%s" "$response" | grep "{\"errors\":\[\"Requested record was not found\"\]}" >/dev/null; then + _info "Adding TXT record" + if _constellix_rest POST "domains/${_domain_id}/records" "[{\"type\":\"txt\",\"add\":true,\"set\":{\"name\":\"${_sub_domain}\",\"ttl\":60,\"roundRobin\":[{\"value\":\"${txtvalue}\"}]}}]"; then + if printf -- "%s" "$response" | grep "{\"success\":\"1 record(s) added, 0 record(s) updated, 0 record(s) deleted\"}" >/dev/null; then + _info "Added" + return 0 + else + _err "Error adding TXT record" + fi + fi else - _err "Error adding TXT record" - return 1 + _record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[0-9]*" | cut -d ':' -f 2) + if _constellix_rest GET "domains/${_domain_id}/records/TXT/${_record_id}"; then + _new_rr_values=$(printf "%s\n" "$response" | _egrep_o '"roundRobin":\[[^]]*\]' | sed "s/\]$/,{\"value\":\"${txtvalue}\"}]/") + _debug _new_rr_values "$_new_rr_values" + _info "Updating TXT record" + if _constellix_rest PUT "domains/${_domain_id}/records/TXT/${_record_id}" "{\"name\":\"${_sub_domain}\",\"ttl\":60,${_new_rr_values}}"; then + if printf -- "%s" "$response" | grep "{\"success\":\"Record.*updated successfully\"}" >/dev/null; then + _info "Updated" + return 0 + elif printf -- "%s" "$response" | grep "{\"errors\":\[\"Contents are identical\"\]}" >/dev/null; then + _info "Already exists, no need to update" + return 0 + else + _err "Error updating TXT record" + fi + fi + fi fi fi + + return 1 } # Usage: fulldomain txtvalue @@ -61,16 +86,26 @@ dns_constellix_rm() { return 1 fi - _info "Removing TXT record" - if _constellix_rest POST "domains/${_domain_id}/records" "[{\"type\":\"txt\",\"delete\":true,\"filter\":{\"field\":\"name\",\"op\":\"eq\",\"value\":\"${_sub_domain}\"}}]"; then - if printf -- "%s" "$response" | grep "{\"success\":\"0 record(s) added, 0 record(s) updated, 1 record(s) deleted\"}" >/dev/null; then + # The TXT record might have been removed already when working with some wildcard certificates. + _debug "Search TXT record" + if _constellix_rest GET "domains/${_domain_id}/records/TXT/search?exact=${_sub_domain}"; then + if printf -- "%s" "$response" | grep "{\"errors\":\[\"Requested record was not found\"\]}" >/dev/null; then _info "Removed" return 0 else - _err "Error removing TXT record" - return 1 + _info "Removing TXT record" + if _constellix_rest POST "domains/${_domain_id}/records" "[{\"type\":\"txt\",\"delete\":true,\"filter\":{\"field\":\"name\",\"op\":\"eq\",\"value\":\"${_sub_domain}\"}}]"; then + if printf -- "%s" "$response" | grep "{\"success\":\"0 record(s) added, 0 record(s) updated, 1 record(s) deleted\"}" >/dev/null; then + _info "Removed" + return 0 + else + _err "Error removing TXT record" + fi + fi fi fi + + return 1 } #################### Private functions below ################################## @@ -91,7 +126,7 @@ _get_root() { fi if _contains "$response" "\"name\":\"$h\""; then - _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[0-9]+" | cut -d ':' -f 2) + _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[0-9]*" | cut -d ':' -f 2) if [ "$_domain_id" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d '.' -f 1-$p) _domain="$h" diff --git a/dnsapi/dns_cpanel.sh b/dnsapi/dns_cpanel.sh new file mode 100755 index 00000000..f91725a4 --- /dev/null +++ b/dnsapi/dns_cpanel.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env sh +# +#Author: Bjarne Saltbaek +#Report Bugs here: https://github.com/acmesh-official/acme.sh/issues/3732 +# +# +######## Public functions ##################### +# +# Export CPANEL username,api token and hostname in the following variables +# +# cPanel_Username=username +# cPanel_Apitoken=apitoken +# cPanel_Hostname=hostname +# +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +dns_cpanel_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Adding TXT record to cPanel based system" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + _debug cPanel_Username "$cPanel_Username" + _debug cPanel_Apitoken "$cPanel_Apitoken" + _debug cPanel_Hostname "$cPanel_Hostname" + + if ! _cpanel_login; then + _err "cPanel Login failed for user $cPanel_Username. Check $HTTP_HEADER file" + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "No matching root domain for $fulldomain found" + return 1 + fi + # adding entry + _info "Adding the entry" + 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 + _err "Couldn't create entry!" + return 1 +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +dns_cpanel_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Using cPanel based system" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _cpanel_login; then + _err "cPanel Login failed for user $cPanel_Username. Check $HTTP_HEADER file" + return 1 + fi + + if ! _get_root; then + _err "No matching root domain for $fulldomain found" + return 1 + fi + + _findentry "$fulldomain" "$txtvalue" + if [ -z "$_id" ]; then + _info "Entry doesn't exist, nothing to delete" + return 0 + fi + _debug "Deleting record..." + _myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=remove_zone_record&domain=$_domain&line=$_id" + # removing entry + _debug "_result is: $_result" + + if _successful_update; then return 0; fi + _err "Couldn't delete entry!" + return 1 +} + +#################### Private functions below ################################## + +_checkcredentials() { + cPanel_Username="${cPanel_Username:-$(_readaccountconf_mutable cPanel_Username)}" + cPanel_Apitoken="${cPanel_Apitoken:-$(_readaccountconf_mutable cPanel_Apitoken)}" + cPanel_Hostname="${cPanel_Hostname:-$(_readaccountconf_mutable cPanel_Hostname)}" + + if [ -z "$cPanel_Username" ] || [ -z "$cPanel_Apitoken" ] || [ -z "$cPanel_Hostname" ]; then + cPanel_Username="" + cPanel_Apitoken="" + cPanel_Hostname="" + _err "You haven't specified cPanel username, apitoken and hostname yet." + _err "Please add credentials and try again." + return 1 + fi + #save the credentials to the account conf file. + _saveaccountconf_mutable cPanel_Username "$cPanel_Username" + _saveaccountconf_mutable cPanel_Apitoken "$cPanel_Apitoken" + _saveaccountconf_mutable cPanel_Hostname "$cPanel_Hostname" + return 0 +} + +_cpanel_login() { + if ! _checkcredentials; then return 1; fi + + if ! _myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=CustInfo&cpanel_jsonapi_func=displaycontactinfo"; then + _err "cPanel login failed for user $cPanel_Username." + return 1 + fi + return 0 +} + +_myget() { + #Adds auth header to request + export _H1="Authorization: cpanel $cPanel_Username:$cPanel_Apitoken" + _result=$(_get "$cPanel_Hostname/$1") +} + +_get_root() { + _myget 'json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=fetchzones' + _domains=$(echo "$_result" | sed 's/.*\(zones.*\[\).*/\1/' | cut -d':' -f2 | sed 's/"//g' | sed 's/{//g') + _debug "_result is: $_result" + _debug "_domains is: $_domains" + if [ -z "$_domains" ]; then + _err "Primary domain list not found!" + return 1 + fi + for _domain in $_domains; do + _debug "Checking if $fulldomain ends with $_domain" + if (_endswith "$fulldomain" "$_domain"); then + _debug "Root domain: $_domain" + return 0 + fi + done + return 1 +} + +_successful_update() { + if (echo "$_result" | grep -q 'newserial'); then return 0; fi + return 1 +} + +_findentry() { + _debug "In _findentry" + #returns id of dns entry, if it exists + _myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=fetchzone_records&domain=$_domain" + _id=$(echo "$_result" | sed "s/.*\(line.*$fulldomain.*$txtvalue\).*/\1/" | cut -d ':' -f 2 | cut -d ',' -f 1) + _debug "_result is: $_result" + _debug "fulldomain. is $fulldomain." + _debug "txtvalue is $txtvalue" + _debug "_id is: $_id" + if [ -n "$_id" ]; then + _debug "Entry found with _id=$_id" + return 0 + fi + return 1 +} diff --git a/dnsapi/dns_desec.sh b/dnsapi/dns_desec.sh index f64660a8..495a6780 100644 --- a/dnsapi/dns_desec.sh +++ b/dnsapi/dns_desec.sh @@ -20,21 +20,17 @@ dns_desec_add() { _debug txtvalue "$txtvalue" DEDYN_TOKEN="${DEDYN_TOKEN:-$(_readaccountconf_mutable DEDYN_TOKEN)}" - DEDYN_NAME="${DEDYN_NAME:-$(_readaccountconf_mutable DEDYN_NAME)}" - if [ -z "$DEDYN_TOKEN" ] || [ -z "$DEDYN_NAME" ]; then + if [ -z "$DEDYN_TOKEN" ]; then DEDYN_TOKEN="" - DEDYN_NAME="" - _err "You did not specify DEDYN_TOKEN and DEDYN_NAME yet." + _err "You did not specify DEDYN_TOKEN yet." _err "Please create your key and try again." _err "e.g." _err "export DEDYN_TOKEN=d41d8cd98f00b204e9800998ecf8427e" - _err "export DEDYN_NAME=foobar.dedyn.io" return 1 fi - #save the api token and name to the account conf file. + #save the api token to the account conf file. _saveaccountconf_mutable DEDYN_TOKEN "$DEDYN_TOKEN" - _saveaccountconf_mutable DEDYN_NAME "$DEDYN_NAME" _debug "First detect the root zone" if ! _get_root "$fulldomain" "$REST_API/"; then @@ -47,7 +43,7 @@ dns_desec_add() { # Get existing TXT record _debug "Getting txt records" txtvalues="\"\\\"$txtvalue\\\"\"" - _desec_rest GET "$REST_API/$DEDYN_NAME/rrsets/$_sub_domain/TXT/" + _desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/" if [ "$_code" = "200" ]; then oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")" @@ -63,7 +59,7 @@ dns_desec_add() { _info "Adding record" body="[{\"subname\":\"$_sub_domain\", \"type\":\"TXT\", \"records\":[$txtvalues], \"ttl\":3600}]" - if _desec_rest PUT "$REST_API/$DEDYN_NAME/rrsets/" "$body"; then + if _desec_rest PUT "$REST_API/$_domain/rrsets/" "$body"; then if _contains "$response" "$txtvalue"; then _info "Added, OK" return 0 @@ -87,16 +83,13 @@ dns_desec_rm() { _debug txtvalue "$txtvalue" DEDYN_TOKEN="${DEDYN_TOKEN:-$(_readaccountconf_mutable DEDYN_TOKEN)}" - DEDYN_NAME="${DEDYN_NAME:-$(_readaccountconf_mutable DEDYN_NAME)}" - if [ -z "$DEDYN_TOKEN" ] || [ -z "$DEDYN_NAME" ]; then + if [ -z "$DEDYN_TOKEN" ]; then DEDYN_TOKEN="" - DEDYN_NAME="" - _err "You did not specify DEDYN_TOKEN and DEDYN_NAME yet." + _err "You did not specify DEDYN_TOKEN yet." _err "Please create your key and try again." _err "e.g." _err "export DEDYN_TOKEN=d41d8cd98f00b204e9800998ecf8427e" - _err "export DEDYN_NAME=foobar.dedyn.io" return 1 fi @@ -112,7 +105,7 @@ dns_desec_rm() { # Get existing TXT record _debug "Getting txt records" txtvalues="" - _desec_rest GET "$REST_API/$DEDYN_NAME/rrsets/$_sub_domain/TXT/" + _desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/" if [ "$_code" = "200" ]; then oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")" @@ -131,7 +124,7 @@ dns_desec_rm() { _info "Deleting record" body="[{\"subname\":\"$_sub_domain\", \"type\":\"TXT\", \"records\":[$txtvalues], \"ttl\":3600}]" - _desec_rest PUT "$REST_API/$DEDYN_NAME/rrsets/" "$body" + _desec_rest PUT "$REST_API/$_domain/rrsets/" "$body" if [ "$_code" = "200" ]; then _info "Deleted, OK" return 0 diff --git a/dnsapi/dns_dp.sh b/dnsapi/dns_dp.sh index 033fa5aa..9b8b7a8b 100755 --- a/dnsapi/dns_dp.sh +++ b/dnsapi/dns_dp.sh @@ -89,7 +89,7 @@ add_record() { _info "Adding record" - if ! _rest POST "Record.Create" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=默认"; then + if ! _rest POST "Record.Create" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=%E9%BB%98%E8%AE%A4"; then return 1 fi diff --git a/dnsapi/dns_duckdns.sh b/dnsapi/dns_duckdns.sh index 618e12c6..d6e1dbdc 100755 --- a/dnsapi/dns_duckdns.sh +++ b/dnsapi/dns_duckdns.sh @@ -12,7 +12,7 @@ DuckDNS_API="https://www.duckdns.org/update" -######## Public functions ##################### +######## Public functions ###################### #Usage: dns_duckdns_add _acme-challenge.domain.duckdns.org "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_duckdns_add() { @@ -112,7 +112,7 @@ _duckdns_rest() { param="$2" _debug param "$param" url="$DuckDNS_API?$param" - if [ "$DEBUG" -gt 0 ]; then + if [ -n "$DEBUG" ] && [ "$DEBUG" -gt 0 ]; then url="$url&verbose=true" fi _debug url "$url" @@ -121,7 +121,7 @@ _duckdns_rest() { if [ "$method" = "GET" ]; then response="$(_get "$url")" _debug2 response "$response" - if [ "$DEBUG" -gt 0 ] && _contains "$response" "UPDATED" && _contains "$response" "OK"; then + if [ -n "$DEBUG" ] && [ "$DEBUG" -gt 0 ] && _contains "$response" "UPDATED" && _contains "$response" "OK"; then response="OK" fi else diff --git a/dnsapi/dns_gcloud.sh b/dnsapi/dns_gcloud.sh index 03060a8c..d560996c 100755 --- a/dnsapi/dns_gcloud.sh +++ b/dnsapi/dns_gcloud.sh @@ -163,5 +163,8 @@ _dns_gcloud_get_rrdatas() { return 1 fi ttl=$(echo "$rrdatas" | cut -f1) - rrdatas=$(echo "$rrdatas" | cut -f2 | sed 's/","/"\n"/g') + # starting with version 353.0.0 gcloud seems to + # separate records with a semicolon instead of commas + # see also https://cloud.google.com/sdk/docs/release-notes#35300_2021-08-17 + rrdatas=$(echo "$rrdatas" | cut -f2 | sed 's/"[,;]"/"\n"/g') } diff --git a/dnsapi/dns_he.sh b/dnsapi/dns_he.sh index ef09fa0a..bf4a5030 100755 --- a/dnsapi/dns_he.sh +++ b/dnsapi/dns_he.sh @@ -85,7 +85,7 @@ dns_he_rm() { _debug "The txt record is not found, just skip" return 0 fi - _record_id="$(echo "$response" | tr -d "#" | sed "s//dev/null | ${ACME_OPENSSL_BIN:-openssl} md5 -c | cut -d = -f 2 | tr -d ' ' + +} + +_signed_request() { + + _sig_method="$1" + _sig_target="$2" + _sig_body="$3" + _return_field="$4" + + _key_fingerprint=$(_fingerprint "$OCI_CLI_KEY") + _sig_host="dns.$OCI_CLI_REGION.oraclecloud.com" + _sig_keyId="$OCI_CLI_TENANCY/$OCI_CLI_USER/$_key_fingerprint" + _sig_alg="rsa-sha256" + _sig_version="1" + _sig_now="$(LC_ALL=C \date -u "+%a, %d %h %Y %H:%M:%S GMT")" + + _request_method=$(printf %s "$_sig_method" | _lower_case) + _curl_method=$(printf %s "$_sig_method" | _upper_case) + + _request_target="(request-target): $_request_method $_sig_target" + _date_header="date: $_sig_now" + _host_header="host: $_sig_host" + + _string_to_sign="$_request_target\n$_date_header\n$_host_header" + _sig_headers="(request-target) date host" + + if [ "$_sig_body" ]; then + _secure_debug3 _sig_body "$_sig_body" + _sig_body_sha256="x-content-sha256: $(printf %s "$_sig_body" | _digest sha256)" + _sig_body_type="content-type: application/json" + _sig_body_length="content-length: ${#_sig_body}" + _string_to_sign="$_string_to_sign\n$_sig_body_sha256\n$_sig_body_type\n$_sig_body_length" + _sig_headers="$_sig_headers x-content-sha256 content-type content-length" + fi + + _tmp_file=$(_mktemp) + if [ -f "$_tmp_file" ]; then + printf '%s' "$OCI_CLI_KEY" >"$_tmp_file" + _signature=$(printf '%b' "$_string_to_sign" | _sign "$_tmp_file" sha256 | tr -d '\r\n') + rm -f "$_tmp_file" + fi + + _signed_header="Authorization: Signature version=\"$_sig_version\",keyId=\"$_sig_keyId\",algorithm=\"$_sig_alg\",headers=\"$_sig_headers\",signature=\"$_signature\"" + _secure_debug3 _signed_header "$_signed_header" + + if [ "$_curl_method" = "GET" ]; then + export _H1="$_date_header" + export _H2="$_signed_header" + _response="$(_get "https://${_sig_host}${_sig_target}")" + elif [ "$_curl_method" = "PATCH" ]; then + export _H1="$_date_header" + export _H2="$_sig_body_sha256" + export _H3="$_sig_body_type" + export _H4="$_sig_body_length" + export _H5="$_signed_header" + _response="$(_post "$_sig_body" "https://${_sig_host}${_sig_target}" "" "PATCH")" + else + _err "Unable to process method: $_curl_method." + fi + + _ret="$?" + if [ "$_return_field" ]; then + _response="$(echo "$_response" | sed 's/\\\"//g'))" + _return=$(echo "${_response}" | _egrep_o "\"$_return_field\"\\s*:\\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"") + else + _return="$_response" + fi + + printf "%s" "$_return" + return $_ret + +} + +# file key [section] +_readini() { + _file="$1" + _key="$2" + _section="${3:-DEFAULT}" + + _start_n=$(grep -n '\['"$_section"']' "$_file" | cut -d : -f 1) + _debug3 _start_n "$_start_n" + if [ -z "$_start_n" ]; then + _err "Can not find section: $_section" + return 1 + fi + + _start_nn=$(_math "$_start_n" + 1) + _debug3 "_start_nn" "$_start_nn" + + _left="$(sed -n "${_start_nn},99999p" "$_file")" + _debug3 _left "$_left" + _end="$(echo "$_left" | grep -n "^\[" | _head_n 1)" + _debug3 "_end" "$_end" + if [ "$_end" ]; then + _end_n=$(echo "$_end" | cut -d : -f 1) + _debug3 "_end_n" "$_end_n" + _seg_n=$(echo "$_left" | sed -n "1,${_end_n}p") + else + _seg_n="$_left" + fi + + _debug3 "_seg_n" "$_seg_n" + _lineini="$(echo "$_seg_n" | grep "^ *$_key *= *")" + _inivalue="$(printf "%b" "$(eval "echo $_lineini | sed \"s/^ *${_key} *= *//g\"")")" + _debug2 _inivalue "$_inivalue" + echo "$_inivalue" + +} diff --git a/dnsapi/dns_one.sh b/dnsapi/dns_one.sh index 890cc804..1565b767 100644 --- a/dnsapi/dns_one.sh +++ b/dnsapi/dns_one.sh @@ -1,22 +1,9 @@ #!/usr/bin/env sh -# -*- mode: sh; tab-width: 2; indent-tabs-mode: s; coding: utf-8 -*- - # one.com ui wrapper for acme.sh -# Author: github: @diseq -# Created: 2019-02-17 -# Fixed by: @der-berni -# Modified: 2020-04-07 -# -# Use ONECOM_KeepCnameProxy to keep the CNAME DNS record -# export ONECOM_KeepCnameProxy="1" + # # export ONECOM_User="username" # export ONECOM_Password="password" -# -# Usage: -# acme.sh --issue --dns dns_one -d example.com -# -# only single domain supported atm dns_one_add() { fulldomain=$1 @@ -36,27 +23,9 @@ dns_one_add() { subdomain="${_sub_domain}" maindomain=${_domain} - useProxy=0 - if [ "${_sub_domain}" = "_acme-challenge" ]; then - subdomain="proxy${_sub_domain}" - useProxy=1 - fi - _debug subdomain "$subdomain" _debug maindomain "$maindomain" - if [ $useProxy -eq 1 ]; then - #Check if the CNAME exists - _dns_one_getrecord "CNAME" "$_sub_domain" "$subdomain.$maindomain" - if [ -z "$id" ]; then - _info "$(__red "Add CNAME Proxy record: '$(__green "\"$_sub_domain\" => \"$subdomain.$maindomain\"")'")" - _dns_one_addrecord "CNAME" "$_sub_domain" "$subdomain.$maindomain" - - _info "Not valid yet, let's wait 1 hour to take effect." - _sleep 3600 - fi - fi - #Check if the TXT exists _dns_one_getrecord "TXT" "$subdomain" "$txtvalue" if [ -n "$id" ]; then @@ -92,26 +61,8 @@ dns_one_rm() { subdomain="${_sub_domain}" maindomain=${_domain} - useProxy=0 - if [ "${_sub_domain}" = "_acme-challenge" ]; then - subdomain="proxy${_sub_domain}" - useProxy=1 - fi - _debug subdomain "$subdomain" _debug maindomain "$maindomain" - if [ $useProxy -eq 1 ]; then - if [ "$ONECOM_KeepCnameProxy" = "1" ]; then - _info "$(__red "Keeping CNAME Proxy record: '$(__green "\"$_sub_domain\" => \"$subdomain.$maindomain\"")'")" - else - #Check if the CNAME exists - _dns_one_getrecord "CNAME" "$_sub_domain" "$subdomain.$maindomain" - if [ -n "$id" ]; then - _info "$(__red "Removing CNAME Proxy record: '$(__green "\"$_sub_domain\" => \"$subdomain.$maindomain\"")'")" - _dns_one_delrecord "$id" - fi - fi - fi #Check if the TXT exists _dns_one_getrecord "TXT" "$subdomain" "$txtvalue" @@ -136,7 +87,7 @@ dns_one_rm() { # _domain=domain.com _get_root() { domain="$1" - i=2 + i=1 p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f $i-100) @@ -163,8 +114,6 @@ _get_root() { _dns_one_login() { # get credentials - ONECOM_KeepCnameProxy="${ONECOM_KeepCnameProxy:-$(_readaccountconf_mutable ONECOM_KeepCnameProxy)}" - ONECOM_KeepCnameProxy="${ONECOM_KeepCnameProxy:-0}" ONECOM_User="${ONECOM_User:-$(_readaccountconf_mutable ONECOM_User)}" ONECOM_Password="${ONECOM_Password:-$(_readaccountconf_mutable ONECOM_Password)}" if [ -z "$ONECOM_User" ] || [ -z "$ONECOM_Password" ]; then @@ -176,7 +125,6 @@ _dns_one_login() { fi #save the api key and email to the account conf file. - _saveaccountconf_mutable ONECOM_KeepCnameProxy "$ONECOM_KeepCnameProxy" _saveaccountconf_mutable ONECOM_User "$ONECOM_User" _saveaccountconf_mutable ONECOM_Password "$ONECOM_Password" diff --git a/dnsapi/dns_ovh.sh b/dnsapi/dns_ovh.sh index f6f9689a..e65babbd 100755 --- a/dnsapi/dns_ovh.sh +++ b/dnsapi/dns_ovh.sh @@ -261,7 +261,9 @@ _get_root() { return 1 fi - if ! _contains "$response" "This service does not exist" >/dev/null && ! _contains "$response" "NOT_GRANTED_CALL" >/dev/null; then + if ! _contains "$response" "This service does not exist" >/dev/null && + ! _contains "$response" "This call has not been granted" >/dev/null && + ! _contains "$response" "NOT_GRANTED_CALL" >/dev/null; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) _domain="$h" return 0 diff --git a/dnsapi/dns_pdns.sh b/dnsapi/dns_pdns.sh index 8f07e8c4..6aa2e953 100755 --- a/dnsapi/dns_pdns.sh +++ b/dnsapi/dns_pdns.sh @@ -103,7 +103,7 @@ set_record() { _build_record_string "$oldchallenge" done - if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}"; then + if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}" "application/json"; then _err "Set txt record error." return 1 fi @@ -126,7 +126,7 @@ rm_record() { if _contains "$_existing_challenges" "$txtvalue"; then #Delete all challenges (PowerDNS API does not allow to delete content) - if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}"; then + if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"DELETE\", \"name\": \"$full.\", \"type\": \"TXT\"}]}" "application/json"; then _err "Delete txt record error." return 1 fi @@ -140,7 +140,7 @@ rm_record() { fi done #Recreate the existing challenges - if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}"; then + if ! _pdns_rest "PATCH" "/api/v1/servers/$PDNS_ServerId/zones/$root" "{\"rrsets\": [{\"changetype\": \"REPLACE\", \"name\": \"$full.\", \"type\": \"TXT\", \"ttl\": $PDNS_Ttl, \"records\": [$_record_string]}]}" "application/json"; then _err "Set txt record error." return 1 fi @@ -175,13 +175,13 @@ _get_root() { i=1 if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones"; then - _zones_response="$response" + _zones_response=$(echo "$response" | _normalizeJson) fi while true; do h=$(printf "%s" "$domain" | cut -d . -f $i-100) - if _contains "$_zones_response" "\"name\": \"$h.\""; then + if _contains "$_zones_response" "\"name\":\"$h.\""; then _domain="$h." if [ -z "$h" ]; then _domain="=2E" @@ -203,12 +203,13 @@ _pdns_rest() { method=$1 ep=$2 data=$3 + ct=$4 export _H1="X-API-Key: $PDNS_Token" if [ ! "$method" = "GET" ]; then _debug data "$data" - response="$(_post "$data" "$PDNS_Url$ep" "" "$method")" + response="$(_post "$data" "$PDNS_Url$ep" "" "$method" "$ct")" else response="$(_get "$PDNS_Url$ep")" fi diff --git a/dnsapi/dns_porkbun.sh b/dnsapi/dns_porkbun.sh new file mode 100644 index 00000000..ad4455b6 --- /dev/null +++ b/dnsapi/dns_porkbun.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env sh + +# +#PORKBUN_API_KEY="pk1_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +#PORKBUN_SECRET_API_KEY="sk1_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + +PORKBUN_Api="https://porkbun.com/api/json/v3" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_porkbun_add() { + fulldomain=$1 + txtvalue=$2 + + PORKBUN_API_KEY="${PORKBUN_API_KEY:-$(_readaccountconf_mutable PORKBUN_API_KEY)}" + PORKBUN_SECRET_API_KEY="${PORKBUN_SECRET_API_KEY:-$(_readaccountconf_mutable PORKBUN_SECRET_API_KEY)}" + + if [ -z "$PORKBUN_API_KEY" ] || [ -z "$PORKBUN_SECRET_API_KEY" ]; then + PORKBUN_API_KEY='' + PORKBUN_SECRET_API_KEY='' + _err "You didn't specify a Porkbun api key and secret api key yet." + _err "You can get yours from here https://porkbun.com/account/api." + return 1 + fi + + #save the credentials to the account conf file. + _saveaccountconf_mutable PORKBUN_API_KEY "$PORKBUN_API_KEY" + _saveaccountconf_mutable PORKBUN_SECRET_API_KEY "$PORKBUN_SECRET_API_KEY" + + _debug 'First detect the root zone' + if ! _get_root "$fulldomain"; then + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + # For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so + # we can not use updating anymore. + # count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) + # _debug count "$count" + # if [ "$count" = "0" ]; then + _info "Adding record" + if _porkbun_rest POST "dns/create/$_domain" "{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}"; then + if _contains "$response" '\"status\":"SUCCESS"'; then + _info "Added, OK" + return 0 + elif _contains "$response" "The record already exists"; then + _info "Already exists, OK" + return 0 + else + _err "Add txt record error. ($response)" + return 1 + fi + fi + _err "Add txt record error." + return 1 + +} + +#fulldomain txtvalue +dns_porkbun_rm() { + fulldomain=$1 + txtvalue=$2 + + PORKBUN_API_KEY="${PORKBUN_API_KEY:-$(_readaccountconf_mutable PORKBUN_API_KEY)}" + PORKBUN_SECRET_API_KEY="${PORKBUN_SECRET_API_KEY:-$(_readaccountconf_mutable PORKBUN_SECRET_API_KEY)}" + + _debug 'First detect the root zone' + if ! _get_root "$fulldomain"; then + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + count=$(echo "$response" | _egrep_o "\"count\": *[^,]*" | cut -d : -f 2 | tr -d " ") + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + record_id=$(echo "$response" | tr '{' '\n' | grep -- "$txtvalue" | cut -d, -f1 | cut -d: -f2 | tr -d \") + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! _porkbun_rest POST "dns/delete/$_domain/$record_id"; then + _err "Delete record error." + return 1 + fi + echo "$response" | tr -d " " | grep '\"status\":"SUCCESS"' >/dev/null + fi + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + return 1 + fi + + if _porkbun_rest POST "dns/retrieve/$h"; then + if _contains "$response" "\"status\":\"SUCCESS\""; then + _domain=$h + _sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")" + return 0 + else + _debug "Go to next level of $_domain" + fi + else + _debug "Go to next level of $_domain" + fi + i=$(_math "$i" + 1) + done + + return 1 +} + +_porkbun_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + api_key_trimmed=$(echo "$PORKBUN_API_KEY" | tr -d '"') + secret_api_key_trimmed=$(echo "$PORKBUN_SECRET_API_KEY" | tr -d '"') + + test -z "$data" && data="{" || data="$(echo $data | cut -d'}' -f1)," + data="$data\"apikey\":\"$api_key_trimmed\",\"secretapikey\":\"$secret_api_key_trimmed\"}" + + export _H1="Content-Type: application/json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$PORKBUN_Api/$ep" "" "$m")" + else + response="$(_get "$PORKBUN_Api/$ep")" + fi + + _sleep 3 # prevent rate limit + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_rackspace.sh b/dnsapi/dns_rackspace.sh index 03e1fa68..b50d9168 100644 --- a/dnsapi/dns_rackspace.sh +++ b/dnsapi/dns_rackspace.sh @@ -7,6 +7,7 @@ RACKSPACE_Endpoint="https://dns.api.rackspacecloud.com/v1.0" +# 20210923 - RS changed the fields in the API response; fix sed # 20190213 - The name & id fields swapped in the API response; fix sed # 20190101 - Duplicating file for new pull request to dev branch # Original - tcocca:rackspace_dnsapi https://github.com/acmesh-official/acme.sh/pull/1297 @@ -79,8 +80,8 @@ _get_root_zone() { _debug2 response "$response" if _contains "$response" "\"name\":\"$h\"" >/dev/null; then # Response looks like: - # {"ttl":300,"accountId":12345,"id":1111111,"name":"example.com","emailAddress": ... - _domain_id=$(echo "$response" | sed -n "s/^.*\"id\":\([^,]*\),\"name\":\"$h\",.*/\1/p") + # {"id":"12345","accountId":"1111111","name": "example.com","ttl":3600,"emailAddress": ... + _domain_id=$(echo "$response" | sed -n "s/^.*\"id\":\"\([^,]*\)\",\"accountId\":\"[0-9]*\",\"name\":\"$h\",.*/\1/p") _debug2 domain_id "$_domain_id" if [ -n "$_domain_id" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) diff --git a/dnsapi/dns_servercow.sh b/dnsapi/dns_servercow.sh index e73d85b0..f70a2294 100755 --- a/dnsapi/dns_servercow.sh +++ b/dnsapi/dns_servercow.sh @@ -49,16 +49,42 @@ dns_servercow_add() { _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - if _servercow_api POST "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":20}"; then - if printf -- "%s" "$response" | grep "ok" >/dev/null; then - _info "Added, OK" - return 0 - else - _err "add txt record error." - return 1 + # check whether a txt record already exists for the subdomain + if printf -- "%s" "$response" | grep "{\"name\":\"$_sub_domain\",\"ttl\":20,\"type\":\"TXT\"" >/dev/null; then + _info "A txt record with the same name already exists." + # trim the string on the left + txtvalue_old=${response#*{\"name\":\"$_sub_domain\",\"ttl\":20,\"type\":\"TXT\",\"content\":\"} + # trim the string on the right + txtvalue_old=${txtvalue_old%%\"*} + + _debug txtvalue_old "$txtvalue_old" + + _info "Add the new txtvalue to the existing txt record." + if _servercow_api POST "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":[\"$txtvalue\",\"$txtvalue_old\"],\"ttl\":20}"; then + if printf -- "%s" "$response" | grep "ok" >/dev/null; then + _info "Added additional txtvalue, OK" + return 0 + else + _err "add txt record error." + return 1 + fi fi + _err "add txt record error." + return 1 + else + _info "There is no txt record with the name yet." + if _servercow_api POST "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":20}"; then + if printf -- "%s" "$response" | grep "ok" >/dev/null; then + _info "Added, OK" + return 0 + else + _err "add txt record error." + return 1 + fi + fi + _err "add txt record error." + return 1 fi - _err "add txt record error." return 1 } diff --git a/dnsapi/dns_simply.sh b/dnsapi/dns_simply.sh index d053dcf6..e0e05017 100644 --- a/dnsapi/dns_simply.sh +++ b/dnsapi/dns_simply.sh @@ -6,9 +6,11 @@ #SIMPLY_ApiKey="apikey" # #SIMPLY_Api="https://api.simply.com/1/[ACCOUNTNAME]/[APIKEY]" - SIMPLY_Api_Default="https://api.simply.com/1" +#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" dns_simply_add() { @@ -171,7 +173,7 @@ _get_root() { return 1 fi - if _contains "$response" '"code":"NOT_FOUND"'; then + if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then _debug "$h not found" else _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) @@ -196,6 +198,12 @@ _simply_add_record() { return 1 fi + if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then + _err "Call to API not sucessfull, see below message for more details" + _err "$response" + return 1 + fi + return 0 } @@ -211,6 +219,12 @@ _simply_delete_record() { return 1 fi + if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then + _err "Call to API not sucessfull, see below message for more details" + _err "$response" + return 1 + fi + return 0 } diff --git a/dnsapi/dns_veesp.sh b/dnsapi/dns_veesp.sh new file mode 100644 index 00000000..b8a41d00 --- /dev/null +++ b/dnsapi/dns_veesp.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env sh + +# bug reports to stepan@plyask.in + +# +# export VEESP_User="username" +# export VEESP_Password="password" + +VEESP_Api="https://secure.veesp.com/api" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_veesp_add() { + fulldomain=$1 + txtvalue=$2 + + VEESP_Password="${VEESP_Password:-$(_readaccountconf_mutable VEESP_Password)}" + VEESP_User="${VEESP_User:-$(_readaccountconf_mutable VEESP_User)}" + VEESP_auth=$(printf "%s" "$VEESP_User:$VEESP_Password" | _base64) + + if [ -z "$VEESP_Password" ] || [ -z "$VEESP_User" ]; then + VEESP_Password="" + VEESP_User="" + _err "You don't specify veesp api key and email yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable VEESP_Password "$VEESP_Password" + _saveaccountconf_mutable VEESP_User "$VEESP_User" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + if VEESP_rest POST "service/$_service_id/dns/$_domain_id/records" "{\"name\":\"$fulldomain\",\"ttl\":1,\"priority\":0,\"type\":\"TXT\",\"content\":\"$txtvalue\"}"; then + if _contains "$response" "\"success\":true"; then + _info "Added" + #todo: check if the record takes effect + return 0 + else + _err "Add txt record error." + return 1 + fi + fi +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +dns_veesp_rm() { + fulldomain=$1 + txtvalue=$2 + + VEESP_Password="${VEESP_Password:-$(_readaccountconf_mutable VEESP_Password)}" + VEESP_User="${VEESP_User:-$(_readaccountconf_mutable VEESP_User)}" + VEESP_auth=$(printf "%s" "$VEESP_User:$VEESP_Password" | _base64) + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + VEESP_rest GET "service/$_service_id/dns/$_domain_id" + + count=$(printf "%s\n" "$response" | _egrep_o "\"type\":\"TXT\",\"content\":\".\"$txtvalue.\"\"" | wc -l | tr -d " ") + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + record_id=$(printf "%s\n" "$response" | _egrep_o "{\"id\":[^}]*\"type\":\"TXT\",\"content\":\".\"$txtvalue.\"\"" | cut -d\" -f4) + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! VEESP_rest DELETE "service/$_service_id/dns/$_domain_id/records/$record_id"; then + _err "Delete record error." + return 1 + fi + _contains "$response" "\"success\":true" + fi +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=2 + p=1 + if ! VEESP_rest GET "dns"; then + return 1 + fi + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if _contains "$response" "\"name\":\"$h\""; then + _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"domain_id\":[^,]*,\"name\":\"$h\"" | cut -d : -f 2 | cut -d , -f 1 | cut -d '"' -f 2) + _debug _domain_id "$_domain_id" + _service_id=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$h\",\"service_id\":[^}]*" | cut -d : -f 3 | cut -d '"' -f 2) + _debug _service_id "$_service_id" + 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 +} + +VEESP_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Accept: application/json" + export _H2="Authorization: Basic $VEESP_auth" + if [ "$m" != "GET" ]; then + _debug data "$data" + export _H3="Content-Type: application/json" + response="$(_post "$data" "$VEESP_Api/$ep" "" "$m")" + else + response="$(_get "$VEESP_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_vultr.sh b/dnsapi/dns_vultr.sh index c7b52e84..84857966 100644 --- a/dnsapi/dns_vultr.sh +++ b/dnsapi/dns_vultr.sh @@ -33,7 +33,7 @@ dns_vultr_add() { _debug 'Getting txt records' _vultr_rest GET "dns/records?domain=$_domain" - if printf "%s\n" "$response" | grep "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then + if printf "%s\n" "$response" | grep -- "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then _err 'Error' return 1 fi @@ -73,12 +73,12 @@ dns_vultr_rm() { _debug 'Getting txt records' _vultr_rest GET "dns/records?domain=$_domain" - if printf "%s\n" "$response" | grep "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then + if printf "%s\n" "$response" | grep -- "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then _err 'Error' return 1 fi - _record_id="$(echo "$response" | tr '{}' '\n' | grep '"TXT"' | grep "$txtvalue" | tr ',' '\n' | grep -i 'RECORDID' | cut -d : -f 2)" + _record_id="$(echo "$response" | tr '{}' '\n' | grep '"TXT"' | grep -- "$txtvalue" | tr ',' '\n' | grep -i 'RECORDID' | cut -d : -f 2)" _debug _record_id "$_record_id" if [ "$_record_id" ]; then _info "Successfully retrieved the record id for ACME challenge." diff --git a/dnsapi/dns_websupport.sh b/dnsapi/dns_websupport.sh new file mode 100644 index 00000000..e824c9c0 --- /dev/null +++ b/dnsapi/dns_websupport.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env sh + +# Acme.sh DNS API wrapper for websupport.sk +# +# Original author: trgo.sk (https://github.com/trgosk) +# Tweaks by: akulumbeg (https://github.com/akulumbeg) +# Report Bugs here: https://github.com/akulumbeg/acme.sh + +# Requirements: API Key and Secret from https://admin.websupport.sk/en/auth/apiKey +# +# WS_ApiKey="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +# (called "Identifier" in the WS Admin) +# +# WS_ApiSecret="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +# (called "Secret key" in the WS Admin) + +WS_Api="https://rest.websupport.sk" + +######## Public functions ##################### + +dns_websupport_add() { + fulldomain=$1 + txtvalue=$2 + + WS_ApiKey="${WS_ApiKey:-$(_readaccountconf_mutable WS_ApiKey)}" + WS_ApiSecret="${WS_ApiSecret:-$(_readaccountconf_mutable WS_ApiSecret)}" + + if [ "$WS_ApiKey" ] && [ "$WS_ApiSecret" ]; then + _saveaccountconf_mutable WS_ApiKey "$WS_ApiKey" + _saveaccountconf_mutable WS_ApiSecret "$WS_ApiSecret" + else + WS_ApiKey="" + WS_ApiSecret="" + _err "You did not specify the API Key and/or API Secret" + _err "You can get the API login credentials from https://admin.websupport.sk/en/auth/apiKey" + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + # For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so + # we can not use updating anymore. + # count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) + # _debug count "$count" + # if [ "$count" = "0" ]; then + _info "Adding record" + if _ws_rest POST "/v1/user/self/zone/$_domain/record" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + elif _contains "$response" "The record already exists"; then + _info "Already exists, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + return 1 + +} + +dns_websupport_rm() { + fulldomain=$1 + txtvalue=$2 + + _debug2 fulldomain "$fulldomain" + _debug2 txtvalue "$txtvalue" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _ws_rest GET "/v1/user/self/zone/$_domain/record" + + if [ "$(printf "%s" "$response" | tr -d " " | grep -c \"items\")" -lt "1" ]; then + _err "Error: $response" + return 1 + fi + + record_line="$(_get_from_array "$response" "$txtvalue")" + _debug record_line "$record_line" + if [ -z "$record_line" ]; then + _info "Don't need to remove." + else + record_id=$(echo "$record_line" | _egrep_o "\"id\": *[^,]*" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d " ") + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! _ws_rest DELETE "/v1/user/self/zone/$_domain/record/$record_id"; then + _err "Delete record error." + return 1 + fi + if [ "$(printf "%s" "$response" | tr -d " " | grep -c \"success\")" -lt "1" ]; then + return 1 + else + return 0 + fi + fi + +} + +#################### Private Functions ################################## + +_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 ! _ws_rest GET "/v1/user/self/zone"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\""; then + _domain_id=$(echo "$response" | _egrep_o "\[.\"id\": *[^,]*" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d " ") + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_ws_rest() { + me=$1 + pa="$2" + da="$3" + + _debug2 api_key "$WS_ApiKey" + _debug2 api_secret "$WS_ApiSecret" + + timestamp=$(_time) + datez="$(_utc_date | sed "s/ /T/" | sed "s/$/+0000/")" + canonical_request="${me} ${pa} ${timestamp}" + signature_hash=$(printf "%s" "$canonical_request" | _hmac sha1 "$(printf "%s" "$WS_ApiSecret" | _hex_dump | tr -d " ")" hex) + basicauth="$(printf "%s:%s" "$WS_ApiKey" "$signature_hash" | _base64)" + + _debug2 method "$me" + _debug2 path "$pa" + _debug2 data "$da" + _debug2 timestamp "$timestamp" + _debug2 datez "$datez" + _debug2 canonical_request "$canonical_request" + _debug2 signature_hash "$signature_hash" + _debug2 basicauth "$basicauth" + + export _H1="Accept: application/json" + export _H2="Content-Type: application/json" + export _H3="Authorization: Basic ${basicauth}" + export _H4="Date: ${datez}" + + _debug2 H1 "$_H1" + _debug2 H2 "$_H2" + _debug2 H3 "$_H3" + _debug2 H4 "$_H4" + + if [ "$me" != "GET" ]; then + _debug2 "${me} $WS_Api${pa}" + _debug data "$da" + response="$(_post "$da" "${WS_Api}${pa}" "" "$me")" + else + _debug2 "GET $WS_Api${pa}" + response="$(_get "$WS_Api${pa}")" + fi + + _debug2 response "$response" + return "$?" +} + +_get_from_array() { + va="$1" + fi="$2" + for i in $(echo "$va" | sed "s/{/ /g"); do + if _contains "$i" "$fi"; then + echo "$i" + break + fi + done +} diff --git a/dnsapi/dns_world4you.sh b/dnsapi/dns_world4you.sh index 9ab406f6..231c34b3 100644 --- a/dnsapi/dns_world4you.sh +++ b/dnsapi/dns_world4you.sh @@ -36,7 +36,6 @@ dns_world4you_add() { export _H1="Cookie: W4YSESSID=$sessid" form=$(_get "$WORLD4YOU_API/$paketnr/dns") formiddp=$(echo "$form" | grep 'AddDnsRecordForm\[uniqueFormIdDP\]' | sed 's/^.*name="AddDnsRecordForm\[uniqueFormIdDP\]" value="\([^"]*\)".*$/\1/') - formidttl=$(echo "$form" | grep 'AddDnsRecordForm\[uniqueFormIdTTL\]' | sed 's/^.*name="AddDnsRecordForm\[uniqueFormIdTTL\]" value="\([^"]*\)".*$/\1/') form_token=$(echo "$form" | grep 'AddDnsRecordForm\[_token\]' | sed 's/^.*name="AddDnsRecordForm\[_token\]" value="\([^"]*\)".*$/\1/') if [ -z "$formiddp" ]; then _err "Unable to parse form" @@ -45,9 +44,7 @@ dns_world4you_add() { _resethttp export ACME_HTTP_NO_REDIRECTS=1 - body="AddDnsRecordForm[name]=$RECORD&AddDnsRecordForm[dnsType][type]=TXT&\ -AddDnsRecordForm[value]=$value&AddDnsRecordForm[aktivPaket]=$paketnr&AddDnsRecordForm[uniqueFormIdDP]=$formiddp&\ -AddDnsRecordForm[uniqueFormIdTTL]=$formidttl&AddDnsRecordForm[_token]=$form_token" + body="AddDnsRecordForm[name]=$RECORD&AddDnsRecordForm[dnsType][type]=TXT&AddDnsRecordForm[value]=$value&AddDnsRecordForm[uniqueFormIdDP]=$formiddp&AddDnsRecordForm[_token]=$form_token" _info "Adding record..." ret=$(_post "$body" "$WORLD4YOU_API/$paketnr/dns" '' POST 'application/x-www-form-urlencoded') _resethttp @@ -101,7 +98,6 @@ dns_world4you_rm() { form=$(_get "$WORLD4YOU_API/$paketnr/dns") formiddp=$(echo "$form" | grep 'DeleteDnsRecordForm\[uniqueFormIdDP\]' | sed 's/^.*name="DeleteDnsRecordForm\[uniqueFormIdDP\]" value="\([^"]*\)".*$/\1/') - formidttl=$(echo "$form" | grep 'DeleteDnsRecordForm\[uniqueFormIdTTL\]' | sed 's/^.*name="DeleteDnsRecordForm\[uniqueFormIdTTL\]" value="\([^"]*\)".*$/\1/') form_token=$(echo "$form" | grep 'DeleteDnsRecordForm\[_token\]' | sed 's/^.*name="DeleteDnsRecordForm\[_token\]" value="\([^"]*\)".*$/\1/') if [ -z "$formiddp" ]; then _err "Unable to parse form" @@ -113,11 +109,9 @@ dns_world4you_rm() { _resethttp export ACME_HTTP_NO_REDIRECTS=1 - body="DeleteDnsRecordForm[recordId]=$recordid&DeleteDnsRecordForm[aktivPaket]=$paketnr&\ -DeleteDnsRecordForm[uniqueFormIdDP]=$formiddp&DeleteDnsRecordForm[uniqueFormIdTTL]=$formidttl&\ -DeleteDnsRecordForm[_token]=$form_token" + body="DeleteDnsRecordForm[recordId]=$recordid&DeleteDnsRecordForm[uniqueFormIdDP]=$formiddp&DeleteDnsRecordForm[_token]=$form_token" _info "Removing record..." - ret=$(_post "$body" "$WORLD4YOU_API/$paketnr/deleteRecord" '' POST 'application/x-www-form-urlencoded') + ret=$(_post "$body" "$WORLD4YOU_API/$paketnr/dns/record/delete" '' POST 'application/x-www-form-urlencoded') _resethttp if _contains "$(_head_n 3 <"$HTTP_HEADER")" '302'; then @@ -190,7 +184,7 @@ _get_paketnr() { fqdn="$1" form="$2" - domains=$(echo "$form" | grep '^ *[A-Za-z0-9_\.-]*\.[A-Za-z0-9_-]*$' | sed 's/^\s*\(\S*\)$/\1/') + domains=$(echo "$form" | grep '^ *[A-Za-z0-9_\.-]*\.[A-Za-z0-9_-]*$' | sed 's/^ *\(.*\)$/\1/') domain='' for domain in $domains; do if _contains "$fqdn" "$domain\$"; then diff --git a/notify/bark.sh b/notify/bark.sh new file mode 100644 index 00000000..bbd5bf34 --- /dev/null +++ b/notify/bark.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env sh + +#Support iOS Bark Notification + +#BARK_API_URL="https://api.day.app/xxxx" +#BARK_SOUND="yyyy" +#BARK_GROUP="zzzz" + +# subject content statusCode +bark_send() { + _subject="$1" + _content="$2" + _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _debug "_subject" "$_subject" + _debug "_content" "$_content" + _debug "_statusCode" "$_statusCode" + + BARK_API_URL="${BARK_API_URL:-$(_readaccountconf_mutable BARK_API_URL)}" + if [ -z "$BARK_API_URL" ]; then + BARK_API_URL="" + _err "You didn't specify a Bark API URL BARK_API_URL yet." + _err "You can download Bark from App Store and get yours." + return 1 + fi + _saveaccountconf_mutable BARK_API_URL "$BARK_API_URL" + + BARK_SOUND="${BARK_SOUND:-$(_readaccountconf_mutable BARK_SOUND)}" + _saveaccountconf_mutable BARK_SOUND "$BARK_SOUND" + + BARK_GROUP="${BARK_GROUP:-$(_readaccountconf_mutable BARK_GROUP)}" + if [ -z "$BARK_GROUP" ]; then + BARK_GROUP="ACME" + _info "The BARK_GROUP is not set, so use the default ACME as group name." + else + _saveaccountconf_mutable BARK_GROUP "$BARK_GROUP" + fi + + _content=$(echo "$_content" | _url_encode) + _subject=$(echo "$_subject" | _url_encode) + + response="$(_get "$BARK_API_URL/$_subject/$_content?sound=$BARK_SOUND&group=$BARK_GROUP")" + + if [ "$?" = "0" ] && _contains "$response" "success"; then + _info "Bark API fired success." + return 0 + fi + + _err "Bark API fired error." + _err "$response" + return 1 +} diff --git a/notify/feishu.sh b/notify/feishu.sh new file mode 100644 index 00000000..18693c2d --- /dev/null +++ b/notify/feishu.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env sh + +#Support feishu webhooks api + +#required +#FEISHU_WEBHOOK="xxxx" + +#optional +#FEISHU_KEYWORD="yyyy" + +# subject content statusCode +feishu_send() { + _subject="$1" + _content="$2" + _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _debug "_subject" "$_subject" + _debug "_content" "$_content" + _debug "_statusCode" "$_statusCode" + + FEISHU_WEBHOOK="${FEISHU_WEBHOOK:-$(_readaccountconf_mutable FEISHU_WEBHOOK)}" + if [ -z "$FEISHU_WEBHOOK" ]; then + FEISHU_WEBHOOK="" + _err "You didn't specify a feishu webhooks FEISHU_WEBHOOK yet." + _err "You can get yours from https://www.feishu.cn" + return 1 + fi + _saveaccountconf_mutable FEISHU_WEBHOOK "$FEISHU_WEBHOOK" + + FEISHU_KEYWORD="${FEISHU_KEYWORD:-$(_readaccountconf_mutable FEISHU_KEYWORD)}" + if [ "$FEISHU_KEYWORD" ]; then + _saveaccountconf_mutable FEISHU_KEYWORD "$FEISHU_KEYWORD" + fi + + _content=$(echo "$_content" | _json_encode) + _subject=$(echo "$_subject" | _json_encode) + _data="{\"msg_type\": \"text\", \"content\": {\"text\": \"[$FEISHU_KEYWORD]\n$_subject\n$_content\"}}" + + response="$(_post "$_data" "$FEISHU_WEBHOOK" "" "POST" "application/json")" + + if [ "$?" = "0" ] && _contains "$response" "StatusCode\":0"; then + _info "feishu webhooks event fired success." + return 0 + fi + + _err "feishu webhooks event fired error." + _err "$response" + return 1 +} diff --git a/notify/mail.sh b/notify/mail.sh index d33fd0d2..656dd371 100644 --- a/notify/mail.sh +++ b/notify/mail.sh @@ -62,7 +62,7 @@ mail_send() { fi contenttype="text/plain; charset=utf-8" - subject="=?UTF-8?B?$(echo "$_subject" | _base64)?=" + subject="=?UTF-8?B?$(printf -- "%b" "$_subject" | _base64)?=" result=$({ _mail_body | eval "$(_mail_cmnd)"; } 2>&1) # shellcheck disable=SC2181 @@ -79,7 +79,7 @@ mail_send() { _mail_bin() { _MAIL_BIN="" - for b in "$MAIL_BIN" sendmail ssmtp mutt mail msmtp; do + for b in $MAIL_BIN sendmail ssmtp mutt mail msmtp; do if _exists "$b"; then _MAIL_BIN="$b" break @@ -131,6 +131,7 @@ _mail_body() { echo "To: $MAIL_TO" echo "Subject: $subject" echo "Content-Type: $contenttype" + echo "MIME-Version: 1.0" echo ;; esac diff --git a/notify/pushbullet.sh b/notify/pushbullet.sh new file mode 100644 index 00000000..ca997c84 --- /dev/null +++ b/notify/pushbullet.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env sh + +#Support for pushbullet.com's api. Push notification, notification sync and message platform for multiple platforms +#PUSHBULLET_TOKEN="" Required, pushbullet application token +#PUSHBULLET_DEVICE="" Optional, Specific device, ignore to send to all devices + +PUSHBULLET_URI="https://api.pushbullet.com/v2/pushes" +pushbullet_send() { + _subject="$1" + _content="$2" + _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _debug "_statusCode" "$_statusCode" + + PUSHBULLET_TOKEN="${PUSHBULLET_TOKEN:-$(_readaccountconf_mutable PUSHBULLET_TOKEN)}" + if [ -z "$PUSHBULLET_TOKEN" ]; then + PUSHBULLET_TOKEN="" + _err "You didn't specify a Pushbullet application token yet." + return 1 + fi + _saveaccountconf_mutable PUSHBULLET_TOKEN "$PUSHBULLET_TOKEN" + + PUSHBULLET_DEVICE="${PUSHBULLET_DEVICE:-$(_readaccountconf_mutable PUSHBULLET_DEVICE)}" + if [ -z "$PUSHBULLET_DEVICE" ]; then + _clearaccountconf_mutable PUSHBULLET_DEVICE + else + _saveaccountconf_mutable PUSHBULLET_DEVICE "$PUSHBULLET_DEVICE" + fi + + export _H1="Content-Type: application/json" + export _H2="Access-Token: ${PUSHBULLET_TOKEN}" + _content="$(printf "*%s*\n" "$_content" | _json_encode)" + _subject="$(printf "*%s*\n" "$_subject" | _json_encode)" + _data="{\"type\": \"note\",\"title\": \"${_subject}\",\"body\": \"${_content}\",\"device_iden\": \"${PUSHBULLET_DEVICE}\"}" + response="$(_post "$_data" "$PUSHBULLET_URI")" + + if [ "$?" != "0" ] || _contains "$response" "\"error_code\""; then + _err "PUSHBULLET send error." + _err "$response" + return 1 + fi + + _info "PUSHBULLET send success." + return 0 +} diff --git a/notify/sendgrid.sh b/notify/sendgrid.sh index 0d5ea3b3..82d3f6c6 100644 --- a/notify/sendgrid.sh +++ b/notify/sendgrid.sh @@ -37,11 +37,19 @@ sendgrid_send() { fi _saveaccountconf_mutable SENDGRID_FROM "$SENDGRID_FROM" + SENDGRID_FROM_NAME="${SENDGRID_FROM_NAME:-$(_readaccountconf_mutable SENDGRID_FROM_NAME)}" + _saveaccountconf_mutable SENDGRID_FROM_NAME "$SENDGRID_FROM_NAME" + export _H1="Authorization: Bearer $SENDGRID_API_KEY" export _H2="Content-Type: application/json" _content="$(echo "$_content" | _json_encode)" - _data="{\"personalizations\": [{\"to\": [{\"email\": \"$SENDGRID_TO\"}]}],\"from\": {\"email\": \"$SENDGRID_FROM\"},\"subject\": \"$_subject\",\"content\": [{\"type\": \"text/plain\", \"value\": \"$_content\"}]}" + + if [ -z "$SENDGRID_FROM_NAME" ]; then + _data="{\"personalizations\": [{\"to\": [{\"email\": \"$SENDGRID_TO\"}]}],\"from\": {\"email\": \"$SENDGRID_FROM\"},\"subject\": \"$_subject\",\"content\": [{\"type\": \"text/plain\", \"value\": \"$_content\"}]}" + else + _data="{\"personalizations\": [{\"to\": [{\"email\": \"$SENDGRID_TO\"}]}],\"from\": {\"email\": \"$SENDGRID_FROM\", \"name\": \"$SENDGRID_FROM_NAME\"},\"subject\": \"$_subject\",\"content\": [{\"type\": \"text/plain\", \"value\": \"$_content\"}]}" + fi response="$(_post "$_data" "https://api.sendgrid.com/v3/mail/send")" if [ "$?" = "0" ] && [ -z "$response" ]; then diff --git a/notify/smtp.sh b/notify/smtp.sh index 6aa37ca3..293c665e 100644 --- a/notify/smtp.sh +++ b/notify/smtp.sh @@ -2,14 +2,398 @@ # support smtp -smtp_send() { - _subject="$1" - _content="$2" - _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped - _debug "_subject" "$_subject" - _debug "_content" "$_content" - _debug "_statusCode" "$_statusCode" +# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/3358 - _err "Not implemented yet." - return 1 +# This implementation uses either curl or Python (3 or 2.7). +# (See also the "mail" notify hook, which supports other ways to send mail.) + +# SMTP_FROM="from@example.com" # required +# SMTP_TO="to@example.com" # required +# SMTP_HOST="smtp.example.com" # required +# SMTP_PORT="25" # defaults to 25, 465 or 587 depending on SMTP_SECURE +# SMTP_SECURE="tls" # one of "none", "ssl" (implicit TLS, TLS Wrapper), "tls" (explicit TLS, STARTTLS) +# SMTP_USERNAME="" # set if SMTP server requires login +# SMTP_PASSWORD="" # set if SMTP server requires login +# SMTP_TIMEOUT="30" # seconds for SMTP operations to timeout +# SMTP_BIN="/path/to/python_or_curl" # default finds first of python3, python2.7, python, pypy3, pypy, curl on PATH + +SMTP_SECURE_DEFAULT="tls" +SMTP_TIMEOUT_DEFAULT="30" + +# subject content statuscode +smtp_send() { + SMTP_SUBJECT="$1" + SMTP_CONTENT="$2" + # UNUSED: _statusCode="$3" # 0: success, 1: error 2($RENEW_SKIP): skipped + + # Load and validate config: + SMTP_BIN="$(_readaccountconf_mutable_default SMTP_BIN)" + if [ -n "$SMTP_BIN" ] && ! _exists "$SMTP_BIN"; then + _err "SMTP_BIN '$SMTP_BIN' does not exist." + return 1 + fi + if [ -z "$SMTP_BIN" ]; then + # Look for a command that can communicate with an SMTP server. + # (Please don't add sendmail, ssmtp, mutt, mail, or msmtp here. + # Those are already handled by the "mail" notify hook.) + for cmd in python3 python2.7 python pypy3 pypy curl; do + if _exists "$cmd"; then + SMTP_BIN="$cmd" + break + fi + done + if [ -z "$SMTP_BIN" ]; then + _err "The smtp notify-hook requires curl or Python, but can't find any." + _err 'If you have one of them, define SMTP_BIN="/path/to/curl_or_python".' + _err 'Otherwise, see if you can use the "mail" notify-hook instead.' + return 1 + fi + fi + _debug SMTP_BIN "$SMTP_BIN" + _saveaccountconf_mutable_default SMTP_BIN "$SMTP_BIN" + + SMTP_FROM="$(_readaccountconf_mutable_default SMTP_FROM)" + SMTP_FROM="$(_clean_email_header "$SMTP_FROM")" + if [ -z "$SMTP_FROM" ]; then + _err "You must define SMTP_FROM as the sender email address." + return 1 + fi + if _email_has_display_name "$SMTP_FROM"; then + _err "SMTP_FROM must be only a simple email address (sender@example.com)." + _err "Change your SMTP_FROM='$SMTP_FROM' to remove the display name." + return 1 + fi + _debug SMTP_FROM "$SMTP_FROM" + _saveaccountconf_mutable_default SMTP_FROM "$SMTP_FROM" + + SMTP_TO="$(_readaccountconf_mutable_default SMTP_TO)" + SMTP_TO="$(_clean_email_header "$SMTP_TO")" + if [ -z "$SMTP_TO" ]; then + _err "You must define SMTP_TO as the recipient email address(es)." + return 1 + fi + if _email_has_display_name "$SMTP_TO"; then + _err "SMTP_TO must be only simple email addresses (to@example.com,to2@example.com)." + _err "Change your SMTP_TO='$SMTP_TO' to remove the display name(s)." + return 1 + fi + _debug SMTP_TO "$SMTP_TO" + _saveaccountconf_mutable_default SMTP_TO "$SMTP_TO" + + SMTP_HOST="$(_readaccountconf_mutable_default SMTP_HOST)" + if [ -z "$SMTP_HOST" ]; then + _err "You must define SMTP_HOST as the SMTP server hostname." + return 1 + fi + _debug SMTP_HOST "$SMTP_HOST" + _saveaccountconf_mutable_default SMTP_HOST "$SMTP_HOST" + + SMTP_SECURE="$(_readaccountconf_mutable_default SMTP_SECURE "$SMTP_SECURE_DEFAULT")" + case "$SMTP_SECURE" in + "none") smtp_port_default="25" ;; + "ssl") smtp_port_default="465" ;; + "tls") smtp_port_default="587" ;; + *) + _err "Invalid SMTP_SECURE='$SMTP_SECURE'. It must be 'ssl', 'tls' or 'none'." + return 1 + ;; + esac + _debug SMTP_SECURE "$SMTP_SECURE" + _saveaccountconf_mutable_default SMTP_SECURE "$SMTP_SECURE" "$SMTP_SECURE_DEFAULT" + + SMTP_PORT="$(_readaccountconf_mutable_default SMTP_PORT "$smtp_port_default")" + case "$SMTP_PORT" in + *[!0-9]*) + _err "Invalid SMTP_PORT='$SMTP_PORT'. It must be a port number." + return 1 + ;; + esac + _debug SMTP_PORT "$SMTP_PORT" + _saveaccountconf_mutable_default SMTP_PORT "$SMTP_PORT" "$smtp_port_default" + + SMTP_USERNAME="$(_readaccountconf_mutable_default SMTP_USERNAME)" + _debug SMTP_USERNAME "$SMTP_USERNAME" + _saveaccountconf_mutable_default SMTP_USERNAME "$SMTP_USERNAME" + + SMTP_PASSWORD="$(_readaccountconf_mutable_default SMTP_PASSWORD)" + _secure_debug SMTP_PASSWORD "$SMTP_PASSWORD" + _saveaccountconf_mutable_default SMTP_PASSWORD "$SMTP_PASSWORD" + + SMTP_TIMEOUT="$(_readaccountconf_mutable_default SMTP_TIMEOUT "$SMTP_TIMEOUT_DEFAULT")" + _debug SMTP_TIMEOUT "$SMTP_TIMEOUT" + _saveaccountconf_mutable_default SMTP_TIMEOUT "$SMTP_TIMEOUT" "$SMTP_TIMEOUT_DEFAULT" + + SMTP_X_MAILER="$(_clean_email_header "$PROJECT_NAME $VER --notify-hook smtp")" + + # Run with --debug 2 (or above) to echo the transcript of the SMTP session. + # Careful: this may include SMTP_PASSWORD in plaintext! + if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_2" ]; then + SMTP_SHOW_TRANSCRIPT="True" + else + SMTP_SHOW_TRANSCRIPT="" + fi + + SMTP_SUBJECT=$(_clean_email_header "$SMTP_SUBJECT") + _debug SMTP_SUBJECT "$SMTP_SUBJECT" + _debug SMTP_CONTENT "$SMTP_CONTENT" + + # Send the message: + case "$(basename "$SMTP_BIN")" in + curl) _smtp_send=_smtp_send_curl ;; + py*) _smtp_send=_smtp_send_python ;; + *) + _err "Can't figure out how to invoke '$SMTP_BIN'." + _err "Check your SMTP_BIN setting." + return 1 + ;; + esac + + if ! smtp_output="$($_smtp_send)"; then + _err "Error sending message with $SMTP_BIN." + if [ -n "$smtp_output" ]; then + _err "$smtp_output" + fi + return 1 + fi + + return 0 +} + +# Strip CR and NL from text to prevent MIME header injection +# text +_clean_email_header() { + printf "%s" "$(echo "$1" | tr -d "\r\n")" +} + +# Simple check for display name in an email address (< > or ") +# email +_email_has_display_name() { + _email="$1" + expr "$_email" : '^.*[<>"]' >/dev/null +} + +## +## curl smtp sending +## + +# Send the message via curl using SMTP_* variables +_smtp_send_curl() { + # Build curl args in $@ + case "$SMTP_SECURE" in + none) + set -- --url "smtp://${SMTP_HOST}:${SMTP_PORT}" + ;; + ssl) + set -- --url "smtps://${SMTP_HOST}:${SMTP_PORT}" + ;; + tls) + set -- --url "smtp://${SMTP_HOST}:${SMTP_PORT}" --ssl-reqd + ;; + *) + # This will only occur if someone adds a new SMTP_SECURE option above + # without updating this code for it. + _err "Unhandled SMTP_SECURE='$SMTP_SECURE' in _smtp_send_curl" + _err "Please re-run with --debug and report a bug." + return 1 + ;; + esac + + set -- "$@" \ + --upload-file - \ + --mail-from "$SMTP_FROM" \ + --max-time "$SMTP_TIMEOUT" + + # Burst comma-separated $SMTP_TO into individual --mail-rcpt args. + _to="${SMTP_TO}," + while [ -n "$_to" ]; do + _rcpt="${_to%%,*}" + _to="${_to#*,}" + set -- "$@" --mail-rcpt "$_rcpt" + done + + _smtp_login="${SMTP_USERNAME}:${SMTP_PASSWORD}" + if [ "$_smtp_login" != ":" ]; then + set -- "$@" --user "$_smtp_login" + fi + + if [ "$SMTP_SHOW_TRANSCRIPT" = "True" ]; then + set -- "$@" --verbose + else + set -- "$@" --silent --show-error + fi + + raw_message="$(_smtp_raw_message)" + + _debug2 "curl command:" "$SMTP_BIN" "$*" + _debug2 "raw_message:\n$raw_message" + + echo "$raw_message" | "$SMTP_BIN" "$@" +} + +# Output an RFC-822 / RFC-5322 email message using SMTP_* variables. +# (This assumes variables have already been cleaned for use in email headers.) +_smtp_raw_message() { + echo "From: $SMTP_FROM" + echo "To: $SMTP_TO" + echo "Subject: $(_mime_encoded_word "$SMTP_SUBJECT")" + echo "Date: $(_rfc2822_date)" + echo "Content-Type: text/plain; charset=utf-8" + echo "X-Mailer: $SMTP_X_MAILER" + echo + echo "$SMTP_CONTENT" +} + +# Convert text to RFC-2047 MIME "encoded word" format if it contains non-ASCII chars +# text +_mime_encoded_word() { + _text="$1" + # (regex character ranges like [a-z] can be locale-dependent; enumerate ASCII chars to avoid that) + _ascii='] $`"'"[!#%&'()*+,./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ~^_abcdefghijklmnopqrstuvwxyz{|}~-" + if expr "$_text" : "^.*[^$_ascii]" >/dev/null; then + # At least one non-ASCII char; convert entire thing to encoded word + printf "%s" "=?UTF-8?B?$(printf "%s" "$_text" | _base64)?=" + else + # Just printable ASCII, no conversion needed + printf "%s" "$_text" + fi +} + +# Output current date in RFC-2822 Section 3.3 format as required in email headers +# (e.g., "Mon, 15 Feb 2021 14:22:01 -0800") +_rfc2822_date() { + # Notes: + # - this is deliberately not UTC, because it "SHOULD express local time" per spec + # - the spec requires weekday and month in the C locale (English), not localized + # - this date format specifier has been tested on Linux, Mac, Solaris and FreeBSD + _old_lc_time="$LC_TIME" + LC_TIME=C + date +'%a, %-d %b %Y %H:%M:%S %z' + LC_TIME="$_old_lc_time" +} + +## +## Python smtp sending +## + +# Send the message via Python using SMTP_* variables +_smtp_send_python() { + _debug "Python version" "$("$SMTP_BIN" --version 2>&1)" + + # language=Python + "$SMTP_BIN" </dev/null; then # shellcheck disable=SC2154 _message=$(printf "%s\n" "$response" | sed -n 's/.*"ok":\([^,]*\).*/\1/p') if [ "$_message" = "true" ]; then