diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..8905a651 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: acmesh +ko_fi: neilpang +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index f7d4d1d7..c9c1b555 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,14 +1,16 @@ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 29ea4e42..4f7ceb47 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,9 @@ \ No newline at end of file diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml new file mode 100644 index 00000000..bf56c1d6 --- /dev/null +++ b/.github/workflows/DNS.yml @@ -0,0 +1,516 @@ +name: DNS +on: + push: + paths: + - 'dnsapi/*.sh' + - '.github/workflows/DNS.yml' + pull_request: + branches: + - 'dev' + paths: + - 'dnsapi/*.sh' + - '.github/workflows/DNS.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + CheckToken: + runs-on: ubuntu-latest + outputs: + hasToken: ${{ steps.step_one.outputs.hasToken }} + steps: + - name: Set the value + id: step_one + run: | + if [ "${{secrets.TokenName1}}" ] ; 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 }} + + Fail: + runs-on: ubuntu-latest + needs: CheckToken + if: "contains(needs.CheckToken.outputs.hasToken, 'false')" + steps: + - name: "Read this: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Test" + run: | + echo "Read this: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Test" + if [ "${{github.repository_owner}}" != "acmesh-official" ]; then + false + fi + + Docker: + runs-on: ubuntu-latest + needs: CheckToken + if: "contains(needs.CheckToken.outputs.hasToken, 'true')" + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v4 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Set env file + run: | + cd ../acmetest + if [ "${{ secrets.TokenName1}}" ] ; then + echo "${{ secrets.TokenName1}}=${{ secrets.TokenValue1}}" >> docker.env + fi + if [ "${{ secrets.TokenName2}}" ] ; then + echo "${{ secrets.TokenName2}}=${{ secrets.TokenValue2}}" >> docker.env + fi + if [ "${{ secrets.TokenName3}}" ] ; then + echo "${{ secrets.TokenName3}}=${{ secrets.TokenValue3}}" >> docker.env + fi + if [ "${{ secrets.TokenName4}}" ] ; then + echo "${{ secrets.TokenName4}}=${{ secrets.TokenValue4}}" >> docker.env + fi + if [ "${{ secrets.TokenName5}}" ] ; then + echo "${{ secrets.TokenName5}}=${{ secrets.TokenValue5}}" >> docker.env + fi + + - name: Run acmetest + run: cd ../acmetest && ./rundocker.sh testall + + + + + MacOS: + runs-on: macos-latest + needs: Docker + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v4 + - name: Install tools + run: brew install socat + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Run acmetest + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + + + + + Windows: + runs-on: windows-latest + needs: MacOS + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - name: Set git to use LF + run: | + git config --global core.autocrlf false + - uses: actions/checkout@v4 + - 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 https://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: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Run acmetest + shell: bash + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + + + + FreeBSD: + runs-on: ubuntu-latest + needs: Windows + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v4 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/freebsd-vm@v1 + with: + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + prepare: pkg install -y socat curl + usesh: true + copyback: false + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + + + + + OpenBSD: + runs-on: ubuntu-latest + needs: FreeBSD + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v4 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/openbsd-vm@v1 + with: + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + prepare: pkg_add socat curl + usesh: true + copyback: false + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + + + + + NetBSD: + runs-on: ubuntu-latest + needs: OpenBSD + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v4 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/netbsd-vm@v1 + with: + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + prepare: | + pkg_add curl socat + usesh: true + copyback: false + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + + + + + DragonFlyBSD: + runs-on: ubuntu-latest + needs: NetBSD + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v4 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/dragonflybsd-vm@v1 + with: + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + prepare: | + pkg install -y curl socat libnghttp2 + usesh: true + copyback: false + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + + + + + + + + Solaris: + runs-on: ubuntu-latest + needs: DragonFlyBSD + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since Solaris doesn't accept the expired ISRG X1 root + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v4 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/solaris-vm@v1 + with: + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + copyback: false + prepare: pkgutil -y -i socat + 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 + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + + + Omnios: + runs-on: ubuntu-latest + needs: Solaris + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since Omnios doesn't accept the expired ISRG X1 root + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v4 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/omnios-vm@v1 + with: + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + copyback: false + prepare: pkg install 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 + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + + diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml new file mode 100644 index 00000000..f360f85c --- /dev/null +++ b/.github/workflows/DragonFlyBSD.yml @@ -0,0 +1,71 @@ +name: DragonFlyBSD +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/DragonFlyBSD.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/DragonFlyBSD.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + DragonFlyBSD: + 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 }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v4 + - uses: vmactions/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/dragonflybsd-vm@v1 + with: + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: | + pkg install -y curl socat libnghttp2 + usesh: true + copyback: false + run: | + cd ../acmetest \ + && ./letest.sh + + diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml new file mode 100644 index 00000000..b90c9ccd --- /dev/null +++ b/.github/workflows/FreeBSD.yml @@ -0,0 +1,76 @@ +name: FreeBSD +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/FreeBSD.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/FreeBSD.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +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: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + ACME_USE_WGET: 1 + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v4 + - uses: vmactions/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/freebsd-vm@v1 + with: + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: pkg install -y socat curl wget + usesh: true + copyback: false + run: | + cd ../acmetest \ + && ./letest.sh + + diff --git a/.github/workflows/Linux.yml b/.github/workflows/Linux.yml new file mode 100644 index 00000000..33e43483 --- /dev/null +++ b/.github/workflows/Linux.yml @@ -0,0 +1,48 @@ +name: Linux +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Linux.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Linux.yml' + + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + + +jobs: + Linux: + strategy: + matrix: + os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "centos:7", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "mageia", "gentoo/stage3"] + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + TEST_ACME_Server: "LetsEncrypt.org_test" + steps: + - uses: actions/checkout@v4 + - name: Clone acmetest + run: | + cd .. \ + && git clone --depth=1 https://github.com/acmesh-official/acmetest.git \ + && cp -r acme.sh acmetest/ + - name: 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..c3f046ab --- /dev/null +++ b/.github/workflows/MacOS.yml @@ -0,0 +1,60 @@ +name: MacOS +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/MacOS.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/MacOS.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +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@v4 + - name: Install tools + run: brew install socat + - name: Clone acmetest + run: | + cd .. \ + && git clone --depth=1 https://github.com/acmesh-official/acmetest.git \ + && cp -r acme.sh acmetest/ + - name: Run acmetest + run: | + cd ../acmetest \ + && sudo --preserve-env ./letest.sh + + diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml new file mode 100644 index 00000000..a5565d09 --- /dev/null +++ b/.github/workflows/NetBSD.yml @@ -0,0 +1,71 @@ +name: NetBSD +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/NetBSD.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/NetBSD.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + + +jobs: + NetBSD: + 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 }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + steps: + - uses: actions/checkout@v4 + - uses: vmactions/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/netbsd-vm@v1 + with: + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN' + nat: | + "8080": "80" + prepare: | + /usr/sbin/pkg_add curl socat + usesh: true + copyback: false + run: | + cd ../acmetest \ + && ./letest.sh + + diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml new file mode 100644 index 00000000..e3da0be8 --- /dev/null +++ b/.github/workflows/Omnios.yml @@ -0,0 +1,75 @@ +name: Omnios +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Omnios.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Omnios.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + Omnios: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + ACME_USE_WGET: 1 + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v4 + - uses: vmactions/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/omnios-vm@v1 + with: + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: pkg install socat wget + copyback: false + run: | + cd ../acmetest \ + && ./letest.sh + + diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml new file mode 100644 index 00000000..e141c47b --- /dev/null +++ b/.github/workflows/OpenBSD.yml @@ -0,0 +1,76 @@ +name: OpenBSD +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/OpenBSD.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/OpenBSD.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + OpenBSD: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + ACME_USE_WGET: 1 + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v4 + - uses: vmactions/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/openbsd-vm@v1 + with: + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: pkg_add socat curl wget libnghttp2 + usesh: true + copyback: false + run: | + cd ../acmetest \ + && ./letest.sh + + diff --git a/.github/workflows/PebbleStrict.yml b/.github/workflows/PebbleStrict.yml new file mode 100644 index 00000000..3f8fdb62 --- /dev/null +++ b/.github/workflows/PebbleStrict.yml @@ -0,0 +1,72 @@ +name: PebbleStrict +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/PebbleStrict.yml' + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/PebbleStrict.yml' + + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + PebbleStrict: + runs-on: ubuntu-latest + env: + TestingDomain: example.com + TestingAltDomains: www.example.com + TEST_ACME_Server: https://localhost:14000/dir + HTTPS_INSECURE: 1 + Le_HTTPPort: 5002 + TEST_LOCAL: 1 + TEST_CA: "Pebble Intermediate CA" + + steps: + - uses: actions/checkout@v4 + - 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: Set up Pebble + run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Run acmetest + run: cd ../acmetest && ./letest.sh + + PebbleStrict_IPCert: + runs-on: ubuntu-latest + env: + TestingDomain: 1.23.45.67 + TEST_ACME_Server: 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@v4 + - name: Install tools + run: sudo apt-get install -y socat + - name: Run Pebble + run: | + docker run --rm -itd --name=pebble \ + -e PEBBLE_VA_ALWAYS_VALID=1 \ + -p 14000:14000 -p 15000:15000 letsencrypt/pebble:latest pebble -config /test/config/pebble-config.json -strict + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Run acmetest + 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..bdd3f040 --- /dev/null +++ b/.github/workflows/Solaris.yml @@ -0,0 +1,75 @@ +name: Solaris +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Solaris.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Solaris.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +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: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + ACME_USE_WGET: 1 + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v4 + - uses: vmactions/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/solaris-vm@v1 + with: + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: pkgutil -y -i socat curl wget + copyback: false + run: | + cd ../acmetest \ + && ./letest.sh + + diff --git a/.github/workflows/Ubuntu.yml b/.github/workflows/Ubuntu.yml new file mode 100644 index 00000000..53cc1060 --- /dev/null +++ b/.github/workflows/Ubuntu.yml @@ -0,0 +1,103 @@ +name: Ubuntu +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Ubuntu.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Ubuntu.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +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: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1 + ACME_USE_WGET: 1 + - TEST_ACME_Server: "ZeroSSL.com" + CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + CA: "ZeroSSL RSA Domain Secure Site CA" + CA_EMAIL: "githubtest@acme.sh" + TEST_PREFERRED_CHAIN: "" + - TEST_ACME_Server: "https://localhost:9000/acme/acme/directory" + CA_ECDSA: "Smallstep Intermediate CA" + CA: "Smallstep Intermediate CA" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: "" + NO_REVOKE: 1 + - TEST_ACME_Server: "https://localhost:9000/acme/acme/directory" + CA_ECDSA: "Smallstep Intermediate CA" + CA: "Smallstep Intermediate CA" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: "" + NO_REVOKE: 1 + TEST_IPCERT: 1 + TestingDomain: "172.17.0.1" + + 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 }} + NO_REVOKE: ${{ matrix.NO_REVOKE }} + TEST_IPCERT: ${{ matrix.TEST_IPCERT }} + TestingDomain: ${{ matrix.TestingDomain }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v4 + - name: Install tools + run: sudo apt-get install -y socat wget + - name: Start StepCA + if: ${{ matrix.TEST_ACME_Server=='https://localhost:9000/acme/acme/directory' }} + run: | + docker run --rm -d \ + -p 9000:9000 \ + -e "DOCKER_STEPCA_INIT_NAME=Smallstep" \ + -e "DOCKER_STEPCA_INIT_DNS_NAMES=localhost,$(hostname -f)" \ + -e "DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT=true" \ + -e "DOCKER_STEPCA_INIT_PASSWORD=test" \ + --name stepca \ + smallstep/step-ca:0.23.1 + + sleep 5 + docker exec stepca bash -c "echo test >test" \ + && docker exec stepca step ca provisioner add acme --type ACME --admin-subject step --admin-password-file=/home/step/test \ + && docker exec stepca kill -1 1 \ + && docker exec stepca cat /home/step/certs/root_ca.crt | sudo bash -c "cat - >>/etc/ssl/certs/ca-certificates.crt" + - name: Clone acmetest + run: | + cd .. \ + && git clone --depth=1 https://github.com/acmesh-official/acmetest.git \ + && cp -r acme.sh acmetest/ + - name: Run acmetest + run: | + cd ../acmetest \ + && sudo --preserve-env ./letest.sh + + diff --git a/.github/workflows/Windows.yml b/.github/workflows/Windows.yml new file mode 100644 index 00000000..61ef5ad8 --- /dev/null +++ b/.github/workflows/Windows.yml @@ -0,0 +1,78 @@ +name: Windows +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Windows.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Windows.yml' + + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + +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@v4 + - 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 https://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 --depth=1 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 new file mode 100644 index 00000000..ea446d84 --- /dev/null +++ b/.github/workflows/dockerhub.yml @@ -0,0 +1,73 @@ + +name: Build DockerHub +on: + push: + branches: + - '*' + tags: + - '*' + paths: + - '**.sh' + - "Dockerfile" + - '.github/workflows/dockerhub.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + +jobs: + CheckToken: + runs-on: ubuntu-latest + outputs: + hasToken: ${{ steps.step_one.outputs.hasToken }} + env: + DOCKER_PASSWORD : ${{ secrets.DOCKER_PASSWORD }} + steps: + - name: Set the value + id: step_one + run: | + if [ "$DOCKER_PASSWORD" ] ; then + echo "hasToken=true" >>$GITHUB_OUTPUT + else + echo "hasToken=false" >>$GITHUB_OUTPUT + fi + - name: Check the value + run: echo ${{ steps.step_one.outputs.hasToken }} + + build: + runs-on: ubuntu-latest + needs: CheckToken + if: "contains(needs.CheckToken.outputs.hasToken, 'true')" + steps: + - name: checkout code + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: login to docker hub + run: | + echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + - name: build and push the image + run: | + DOCKER_IMAGE=neilpang/acme.sh + + if [[ $GITHUB_REF == refs/tags/* ]]; then + DOCKER_IMAGE_TAG=${GITHUB_REF#refs/tags/} + fi + + if [[ $GITHUB_REF == refs/heads/* ]]; then + DOCKER_IMAGE_TAG=${GITHUB_REF#refs/heads/} + + if [[ $DOCKER_IMAGE_TAG == master ]]; then + DOCKER_IMAGE_TAG=latest + AUTO_UPGRADE=1 + fi + fi + + docker buildx build \ + --tag ${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \ + --output "type=image,push=true" \ + --build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \ + --platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x . diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml new file mode 100644 index 00000000..e92b0411 --- /dev/null +++ b/.github/workflows/issue.yml @@ -0,0 +1,19 @@ +name: "Update issues" +on: + issues: + types: [opened] + +jobs: + comment: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v6 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you." + + }) \ No newline at end of file diff --git a/.github/workflows/pr_dns.yml b/.github/workflows/pr_dns.yml new file mode 100644 index 00000000..b627d3ab --- /dev/null +++ b/.github/workflows/pr_dns.yml @@ -0,0 +1,31 @@ +name: Check dns api + +on: + pull_request_target: + types: + - opened + branches: + - 'dev' + paths: + - 'dnsapi/*.sh' + + +jobs: + welcome: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v6 + with: + script: | + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `**Welcome** + Please make sure you've read our [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide) and [DNS-API-Test](../wiki/DNS-API-Test). + Then reply on this message, otherwise, your code will not be reviewed or merged. + We look forward to reviewing your Pull request shortly ✨ + 注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试. + ` + }) + diff --git a/.github/workflows/pr_notify.yml b/.github/workflows/pr_notify.yml new file mode 100644 index 00000000..3b0e3e4b --- /dev/null +++ b/.github/workflows/pr_notify.yml @@ -0,0 +1,30 @@ +name: Check dns api + +on: + pull_request_target: + types: + - opened + branches: + - 'dev' + paths: + - 'notify/*.sh' + + +jobs: + welcome: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v6 + with: + script: | + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `**Welcome** + Please make sure you've read our [Code-of-conduct](../wiki/Code-of-conduct) and add the usage here: [notify](../wiki/notify). + Then reply on this message, otherwise, your code will not be reviewed or merged. + We look forward to reviewing your Pull request shortly ✨ + ` + }) + diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 00000000..746727d4 --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,38 @@ +name: Shellcheck +on: + push: + branches: + - '*' + paths: + - '**.sh' + - '.github/workflows/shellcheck.yml' + pull_request: + branches: + - dev + paths: + - '**.sh' + - '.github/workflows/shellcheck.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + +jobs: + ShellCheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Shellcheck + run: sudo apt-get install -y shellcheck + - name: DoShellcheck + run: shellcheck -V && shellcheck -e SC2181 -e SC2089 **/*.sh && echo "shellcheck OK" + + shfmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install shfmt + run: curl -sSL https://github.com/mvdan/sh/releases/download/v3.1.2/shfmt_v3.1.2_linux_amd64 -o ~/shfmt && chmod +x ~/shfmt + - name: shfmt + run: ~/shfmt -l -w -i 2 . ; git diff --exit-code && echo "shfmt OK" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 04de1934..00000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -language: shell -sudo: required -dist: trusty - -os: - - linux - - osx - -services: - - docker - -env: - global: - - SHFMT_URL=https://github.com/mvdan/sh/releases/download/v0.4.0/shfmt_v0.4.0_linux_amd64 - - -install: - - if [ "$TRAVIS_OS_NAME" = 'osx' ]; then - brew update && brew install socat; - export PATH="/usr/local/opt/openssl@1.1/bin:$PATH" ; - fi - -script: - - echo "NGROK_TOKEN=$(echo "$NGROK_TOKEN" | wc -c)" - - command -V openssl && openssl version - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then curl -sSL $SHFMT_URL -o ~/shfmt && chmod +x ~/shfmt && ~/shfmt -l -w -i 2 . ; fi - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then git diff --exit-code && echo "shfmt OK" ; fi - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then shellcheck -V ; fi - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then shellcheck -e SC2181 **/*.sh && echo "shellcheck OK" ; fi - - cd .. - - git clone https://github.com/Neilpang/acmetest.git && cp -r acme.sh acmetest/ && cd acmetest - - if [ "$TRAVIS_OS_NAME" = "linux" -a "$NGROK_TOKEN" ]; then sudo TEST_LOCAL="$TEST_LOCAL" NGROK_TOKEN="$NGROK_TOKEN" ./rundocker.sh testplat ubuntu:latest ; fi - - if [ "$TRAVIS_OS_NAME" = "osx" -a "$NGROK_TOKEN" ]; then sudo TEST_LOCAL="$TEST_LOCAL" NGROK_TOKEN="$NGROK_TOKEN" ACME_OPENSSL_BIN="$ACME_OPENSSL_BIN" ./letest.sh ; fi - -matrix: - fast_finish: true - - diff --git a/Dockerfile b/Dockerfile index 5a64c720..2ad50e6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,34 @@ -FROM alpine:3.6 +FROM alpine:3.17 -RUN apk update -f \ - && apk --no-cache add -f \ +RUN apk --no-cache add -f \ openssl \ + openssh-client \ coreutils \ + bind-tools \ curl \ + sed \ socat \ - && rm -rf /var/cache/apk/* + tzdata \ + oath-toolkit-oathtool \ + tar \ + libidn \ + jq \ + cronie ENV LE_CONFIG_HOME /acme.sh -ENV AUTO_UPGRADE 1 +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/ -RUN ln -s /root/.acme.sh/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null##' | crontab - +RUN ln -s /root/.acme.sh/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab - -RUN for verb in help \ +RUN for verb in help \ version \ install \ uninstall \ @@ -33,6 +42,7 @@ RUN for verb in help \ revoke \ remove \ list \ + info \ showcsr \ install-cronjob \ uninstall-cronjob \ @@ -46,17 +56,19 @@ RUN for verb in help \ createCSR \ deactivate \ 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 RUN printf "%b" '#!'"/usr/bin/env sh\n \ if [ \"\$1\" = \"daemon\" ]; then \n \ - trap \"echo stop && killall crond && exit 0\" SIGTERM SIGINT \n \ - crond && while true; do sleep 1; done;\n \ + exec crond -n -s -m off \n \ else \n \ exec -- \"\$@\"\n \ -fi" >/entry.sh && chmod +x /entry.sh +fi\n" >/entry.sh && chmod +x /entry.sh VOLUME /acme.sh diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index b9a5cc59..9a5c106b 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,135 @@ -# An ACME Shell script: acme.sh [![Build Status](https://travis-ci.org/Neilpang/acme.sh.svg?branch=master)](https://travis-ci.org/Neilpang/acme.sh) +# An ACME Shell script: acme.sh +[![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml) +[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml) +[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml) +[![MacOS](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml) +[![Ubuntu](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml) +[![Windows](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml) +[![Solaris](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml) +[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml) +[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml) + +![Shellcheck](https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg) +![PebbleStrict](https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg) +![DockerHub](https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg) + + + [![Join the chat at https://gitter.im/acme-sh/Lobby](https://badges.gitter.im/acme-sh/Lobby.svg)](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Docker stars](https://img.shields.io/docker/stars/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub") +[![Docker pulls](https://img.shields.io/docker/pulls/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub") + + + - 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. -- Simplest shell script for Let's Encrypt free certificate client. -- 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/Neilpang/acme.sh/wiki +Wiki: https://github.com/acmesh-official/acme.sh/wiki -For Docker Fans: [acme.sh :two_hearts: Docker ](https://github.com/Neilpang/acme.sh/wiki/Run-acme.sh-in-docker) +For Docker Fans: [acme.sh :two_hearts: Docker ](https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker) Twitter: [@neilpangxa](https://twitter.com/neilpangxa) -# [中文说明](https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E) +# [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E) # Who: - [FreeBSD.org](https://blog.crashed.org/letsencrypt-in-freebsd-org/) - [ruby-china.org](https://ruby-china.org/topics/31983) -- [Proxmox](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x_and_newer)) +- [Proxmox](https://pve.proxmox.com/wiki/Certificate_Management) - [pfsense](https://github.com/pfsense/FreeBSD-ports/pull/89) -- [webfaction](https://community.webfaction.com/questions/19988/using-letsencrypt) - [Loadbalancer.org](https://www.loadbalancer.org/blog/loadbalancer-org-with-lets-encrypt-quick-and-dirty) - [discourse.org](https://meta.discourse.org/t/setting-up-lets-encrypt/40709) - [Centminmod](https://centminmod.com/letsencrypt-acmetool-https.html) - [splynx](https://forum.splynx.com/t/free-ssl-cert-for-splynx-lets-encrypt/297) -- [archlinux](https://aur.archlinux.org/packages/acme.sh-git/) - [opnsense.org](https://github.com/opnsense/plugins/tree/master/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient) -- [CentOS Web Panel](http://centos-webpanel.com/) +- [CentOS Web Panel](https://control-webpanel.com) - [lnmp.org](https://lnmp.org/) -- [more...](https://github.com/Neilpang/acme.sh/wiki/Blogs-and-tutorials) +- [more...](https://github.com/acmesh-official/acme.sh/wiki/Blogs-and-tutorials) # Tested OS | NO | Status| Platform| |----|-------|---------| -|1|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/ubuntu-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Ubuntu -|2|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/debian-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Debian -|3|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/centos-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|CentOS -|4|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/windows-cygwin.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Windows (cygwin with curl, openssl and crontab included) -|5|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/freebsd.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|FreeBSD -|6|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/pfsense.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|pfsense -|7|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/opensuse-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|openSUSE -|8|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/alpine-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Alpine Linux (with curl) -|9|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/base-archlinux.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Archlinux -|10|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/fedora-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|fedora -|11|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/kalilinux-kali-linux-docker.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Kali Linux -|12|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/oraclelinux-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Oracle Linux -|13|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/proxmox.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Proxmox https://pve.proxmox.com/wiki/HTTPSCertificateConfiguration#Let.27s_Encrypt_using_acme.sh -|14|-----| Cloud Linux https://github.com/Neilpang/le/issues/111 -|15|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/openbsd.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|OpenBSD -|16|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/mageia.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Mageia -|17|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/Neilpang/acme.sh/wiki/How-to-run-on-OpenWRT) -|18|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/solaris.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|SunOS/Solaris -|19|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/gentoo-stage3-amd64.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Gentoo Linux -|20|[![Build Status](https://travis-ci.org/Neilpang/acme.sh.svg?branch=master)](https://travis-ci.org/Neilpang/acme.sh)|Mac OSX +|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|[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)|OpenBSD +|8|[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)|NetBSD +|9|[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)|DragonFlyBSD +|10|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios +|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian +|12|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|CentOS +|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE +|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl) +|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux +|16|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora +|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux +|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux +|19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia +|10|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux +|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|ClearLinux +|22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 +|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) +|24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) -For all build statuses, check our [weekly build project](https://github.com/Neilpang/acmetest): -https://github.com/Neilpang/acmetest +Check our [testing project](https://github.com/acmesh-official/acmetest): +https://github.com/acmesh-official/acmetest + +# Supported 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) +- [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) +- [Pebble strict Mode](https://github.com/letsencrypt/pebble) +- Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA # Supported modes - Webroot mode - Standalone mode +- Standalone tls-alpn mode - Apache mode - Nginx mode - DNS mode -- [DNS alias mode](https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode) -- [Stateless mode](https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode) +- [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) +- [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) # 1. How to install ### 1. Install online -Check this project: https://github.com/Neilpang/get.acme.sh +Check this project: https://github.com/acmesh-official/get.acme.sh ```bash -curl https://get.acme.sh | sh +curl https://get.acme.sh | sh -s email=my@example.com ``` Or: ```bash -wget -O - https://get.acme.sh | sh +wget -O - https://get.acme.sh | sh -s email=my@example.com ``` @@ -104,14 +138,14 @@ wget -O - https://get.acme.sh | sh Clone this project and launch installation: ```bash -git clone https://github.com/Neilpang/acme.sh.git +git clone https://github.com/acmesh-official/acme.sh.git cd ./acme.sh -./acme.sh --install +./acme.sh --install -m my@example.com ``` You `don't have to be root` then, although `it is recommended`. -Advanced Installation: https://github.com/Neilpang/acme.sh/wiki/How-to-install +Advanced Installation: https://github.com/acmesh-official/acme.sh/wiki/How-to-install The installer will perform 3 actions: @@ -173,7 +207,7 @@ The certs will be placed in `~/.acme.sh/example.com/` The certs will be renewed automatically every **60** days. -More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert +More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert # 3. Install the cert to Apache/Nginx etc. @@ -219,14 +253,26 @@ Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com ``` -More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert +More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert + +# 5. Use Standalone ssl server to issue cert + +**(requires you to be root/sudoer or have permission to listen on port 443 (TCP))** + +Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again. + +```bash +acme.sh --issue --alpn -d example.com -d www.example.com -d cp.example.com +``` + +More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert -# 5. Use Apache mode +# 6. Use Apache mode **(requires you to be root/sudoer, since it is required to interact with Apache server)** -If you are running a web server, Apache or Nginx, it is recommended to use the `Webroot mode`. +If you are running a web server, it is recommended to use the `Webroot mode`. Particularly, if you are running an Apache server, you can use Apache mode instead. This mode doesn't write any files to your web root folder. @@ -236,17 +282,17 @@ Just set string "apache" as the second argument and it will force use of apache acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com ``` -**This apache mode is only to issue the cert, it will not change your apache config files. +**This apache mode is only to issue the cert, it will not change your apache config files. You will need to configure your website config files to use the cert by yourself. -We don't want to mess your apache server, don't worry.** +We don't want to mess with your apache server, don't worry.** -More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert +More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert -# 6. Use Nginx mode +# 7. Use Nginx mode **(requires you to be root/sudoer, since it is required to interact with Nginx server)** -If you are running a web server, Apache or Nginx, it is recommended to use the `Webroot mode`. +If you are running a web server, it is recommended to use the `Webroot mode`. Particularly, if you are running an nginx server, you can use nginx mode instead. This mode doesn't write any files to your web root folder. @@ -260,91 +306,27 @@ So, the config is not changed. acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com ``` -**This nginx mode is only to issue the cert, it will not change your nginx config files. +**This nginx mode is only to issue the cert, it will not change your nginx config files. You will need to configure your website config files to use the cert by yourself. -We don't want to mess your nginx server, don't worry.** +We don't want to mess with your nginx server, don't worry.** -More examples: https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert +More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert -# 7. Automatic DNS API integration +# 8. Automatic DNS API integration If your DNS provider supports API access, we can use that API to automatically issue the certs. You don't have to do anything manually! -### Currently acme.sh supports: +### Currently acme.sh supports most of the dns providers: -1. CloudFlare.com API -1. DNSPod.cn API -1. CloudXNS.com API -1. GoDaddy.com API -1. PowerDNS.com API -1. OVH, kimsufi, soyoustart and runabove API -1. nsupdate API -1. LuaDNS.com API -1. DNSMadeEasy.com API -1. AWS Route 53 -1. aliyun.com(阿里云) API -1. ISPConfig 3.1 API -1. Alwaysdata.com API -1. Linode.com API -1. FreeDNS (https://freedns.afraid.org/) -1. cyon.ch -1. Domain-Offensive/Resellerinterface/Domainrobot API -1. Gandi LiveDNS API -1. Knot DNS API -1. DigitalOcean API (native) -1. ClouDNS.net API -1. Infoblox NIOS API (https://www.infoblox.com/) -1. VSCALE (https://vscale.io/) -1. Dynu API (https://www.dynu.com) -1. DNSimple API -1. NS1.com API -1. DuckDNS.org API -1. Name.com API -1. Dyn Managed DNS API -1. Yandex PDD API (https://pdd.yandex.ru) -1. Hurricane Electric DNS service (https://dns.he.net) -1. UnoEuro API (https://www.unoeuro.com/) -1. INWX (https://www.inwx.de/) -1. Servercow (https://servercow.de) -1. Namesilo (https://www.namesilo.com) -1. InternetX autoDNS API (https://internetx.com) -1. Azure DNS -1. selectel.com(selectel.ru) DNS API -1. zonomi.com DNS API -1. DreamHost.com API -1. DirectAdmin API -1. KingHost (https://www.kinghost.com.br/) -1. Zilore (https://zilore.com) -1. Loopia.se API -1. acme-dns (https://github.com/joohoi/acme-dns) -1. TELE3 (https://www.tele3.cz) -1. EUSERV.EU (https://www.euserv.eu) -1. DNSPod.com API (https://www.dnspod.com) -1. Google Cloud DNS API -1. ConoHa (https://www.conoha.jp) -1. netcup DNS API (https://www.netcup.de) -1. GratisDNS.dk (https://gratisdns.dk) -1. Namecheap API (https://www.namecheap.com/) +https://github.com/acmesh-official/acme.sh/wiki/dnsapi -And: +# 9. Use DNS manual mode: -**lexicon DNS API: https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api - (DigitalOcean, DNSimple, DNSMadeEasy, DNSPark, EasyDNS, Namesilo, NS1, PointHQ, Rage4 and Vultr etc.)** +See: https://github.com/acmesh-official/acme.sh/wiki/dns-manual-mode first. - -**More APIs coming soon...** - -If your DNS provider is not on the supported list above, you can write your own DNS API script easily. If you do, please consider submitting a [Pull Request](https://github.com/Neilpang/acme.sh/pulls) and contribute it to the project. - -For more details: [How to use DNS API](dnsapi) - -# 8. Use DNS manual mode: - -See: https://github.com/Neilpang/acme.sh/wiki/dns-manual-mode first. - -If your dns provider doesn't support any api access, you can add the txt record by your hand. +If your dns provider doesn't support any api access, you can add the txt record by hand. ```bash acme.sh --issue --dns -d example.com -d www.example.com -d cp.example.com @@ -376,11 +358,7 @@ Ok, it's done. **Please use dns api mode instead.** -# 9. Issue ECC certificates - -`Let's Encrypt` can now issue **ECDSA** certificates. - -And we support them too! +# 10. Issue ECC certificates Just set the `keylength` parameter with a prefix `ec-`. @@ -402,13 +380,15 @@ Please look at the `keylength` parameter above. Valid values are: -1. **ec-256 (prime256v1, "ECDSA P-256")** +1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)** 2. **ec-384 (secp384r1, "ECDSA P-384")** 3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)** +4. **2048 (RSA2048)** +5. **3072 (RSA3072)** +6. **4096 (RSA4096)** - -# 10. Issue Wildcard certificates +# 11. Issue Wildcard certificates It's simple, just give a wildcard domain as the `-d` parameter. @@ -418,7 +398,7 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf -# 11. How to renew the certs +# 12. How to renew the certs No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days. @@ -435,7 +415,7 @@ acme.sh --renew -d example.com --force --ecc ``` -# 12. How to stop cert renewal +# 13. How to stop cert renewal To stop renewal of a cert, you can execute the following to remove the cert from the renewal list: @@ -448,7 +428,7 @@ The cert/key file is not removed from the disk. You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself. -# 13. How to upgrade `acme.sh` +# 14. How to upgrade `acme.sh` acme.sh is in constant development, so it's strongly recommended to use the latest code. @@ -473,36 +453,73 @@ acme.sh --upgrade --auto-upgrade 0 ``` -# 14. Issue a cert from an existing CSR +# 15. Issue a cert from an existing CSR -https://github.com/Neilpang/acme.sh/wiki/Issue-a-cert-from-existing-CSR +https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR -# 15. Under the Hood +# 16. Send notifications in cronjob + +https://github.com/acmesh-official/acme.sh/wiki/notify + + +# 17. Under the Hood Speak ACME language using shell, directly to "Let's Encrypt". TODO: -# 16. Acknowledgments +# 18. Acknowledgments 1. Acme-tiny: https://github.com/diafygi/acme-tiny 2. ACME protocol: https://github.com/ietf-wg-acme/acme -# 17. License & Others +## Contributors + +### Code Contributors + +This project exists thanks to all the people who contribute. + + +### Financial Contributors + +Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)] + +#### Individuals + + + +#### Organizations + +Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/acmesh/contribute)] + + + + + + + + + + + + + + +# 19. License & Others License is GPLv3 Please Star and Fork me. -[Issues](https://github.com/Neilpang/acme.sh/issues) and [pull requests](https://github.com/Neilpang/acme.sh/pulls) are welcome. +[Issues](https://github.com/acmesh-official/acme.sh/issues) and [pull requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome. -# 18. Donate +# 20. Donate Your donation makes **acme.sh** better: 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/) - -[Donate List](https://github.com/Neilpang/acme.sh/wiki/Donate-list) + +[Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list) diff --git a/acme.sh b/acme.sh index 7944d5df..75030acd 100755 --- a/acme.sh +++ b/acme.sh @@ -1,53 +1,78 @@ #!/usr/bin/env sh -VER=2.8.0 +VER=3.0.8 PROJECT_NAME="acme.sh" PROJECT_ENTRY="acme.sh" -PROJECT="https://github.com/Neilpang/$PROJECT_NAME" +PROJECT="https://github.com/acmesh-official/$PROJECT_NAME" DEFAULT_INSTALL_HOME="$HOME/.$PROJECT_NAME" + +_WINDOWS_SCHEDULER_NAME="$PROJECT_NAME.cron" + _SCRIPT_="$0" -_SUB_FOLDERS="dnsapi deploy" +_SUB_FOLDER_NOTIFY="notify" +_SUB_FOLDER_DNSAPI="dnsapi" +_SUB_FOLDER_DEPLOY="deploy" -LETSENCRYPT_CA_V1="https://acme-v01.api.letsencrypt.org/directory" -LETSENCRYPT_STAGING_CA_V1="https://acme-staging.api.letsencrypt.org/directory" +_SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY" -LETSENCRYPT_CA_V2="https://acme-v02.api.letsencrypt.org/directory" -LETSENCRYPT_STAGING_CA_V2="https://acme-staging-v02.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" -DEFAULT_CA=$LETSENCRYPT_CA_V1 -DEFAULT_STAGING_CA=$LETSENCRYPT_STAGING_CA_V1 +CA_BUYPASS="https://api.buypass.com/acme/directory" +CA_BUYPASS_TEST="https://api.test4.buypass.no/acme/directory" + +CA_ZEROSSL="https://acme.zerossl.com/v2/DV90" +_ZERO_EAB_ENDPOINT="https://api.zerossl.com/acme/eab-credentials-email" + +CA_SSLCOM_RSA="https://acme.ssl.com/sslcom-dv-rsa" +CA_SSLCOM_ECC="https://acme.ssl.com/sslcom-dv-ecc" + +CA_GOOGLE="https://dv.acme-v02.api.pki.goog/directory" +CA_GOOGLE_TEST="https://dv.acme-v02.test-api.pki.goog/directory" + +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 +SSL.com,sslcom +Google.com,google +Google.com_test,googletest,google_test +" + +CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST" DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)" -DEFAULT_ACCOUNT_EMAIL="" -DEFAULT_ACCOUNT_KEY_LENGTH=2048 -DEFAULT_DOMAIN_KEY_LENGTH=2048 +DEFAULT_ACCOUNT_KEY_LENGTH=ec-256 +DEFAULT_DOMAIN_KEY_LENGTH=ec-256 DEFAULT_OPENSSL_BIN="openssl" -_OLD_CA_HOST="https://acme-v01.api.letsencrypt.org" -_OLD_STAGE_CA_HOST="https://acme-staging.api.letsencrypt.org" - VTYPE_HTTP="http-01" VTYPE_DNS="dns-01" -VTYPE_TLS="tls-sni-01" -VTYPE_TLS2="tls-sni-02" +VTYPE_ALPN="tls-alpn-01" + +ID_TYPE_DNS="dns" +ID_TYPE_IP="ip" LOCAL_ANY_ADDRESS="0.0.0.0" -MAX_RENEW=60 - -DEFAULT_DNS_SLEEP=120 +DEFAULT_RENEW=60 NO_VALUE="no" -W_TLS="tls" W_DNS="dns" +W_ALPN="alpn" DNS_ALIAS_PREFIX="=" MODE_STATELESS="stateless" @@ -58,14 +83,18 @@ NGINX="nginx:" NGINX_START="#ACME_NGINX_START" NGINX_END="#ACME_NGINX_END" -BEGIN_CSR="-----BEGIN CERTIFICATE REQUEST-----" -END_CSR="-----END CERTIFICATE REQUEST-----" +BEGIN_CSR="-----BEGIN [NEW ]\{0,4\}CERTIFICATE REQUEST-----" +END_CSR="-----END [NEW ]\{0,4\}CERTIFICATE REQUEST-----" BEGIN_CERT="-----BEGIN CERTIFICATE-----" END_CERT="-----END CERTIFICATE-----" CONTENT_TYPE_JSON="application/jose+json" RENEW_SKIP=2 +CODE_DNS_MANUAL=3 + +B64CONF_START="__ACME_BASE64__START_" +B64CONF_END="__ACME_BASE64__END_" ECC_SEP="_" ECC_SUFFIX="${ECC_SEP}ecc" @@ -73,14 +102,19 @@ ECC_SUFFIX="${ECC_SEP}ecc" LOG_LEVEL_1=1 LOG_LEVEL_2=2 LOG_LEVEL_3=3 -DEFAULT_LOG_LEVEL="$LOG_LEVEL_1" +DEFAULT_LOG_LEVEL="$LOG_LEVEL_2" DEBUG_LEVEL_1=1 DEBUG_LEVEL_2=2 DEBUG_LEVEL_3=3 -DEBUG_LEVEL_DEFAULT=$DEBUG_LEVEL_1 +DEBUG_LEVEL_DEFAULT=$DEBUG_LEVEL_2 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)" SYSLOG_ERROR="user.error" @@ -102,15 +136,49 @@ SYSLOG_LEVEL_DEFAULT=$SYSLOG_LEVEL_ERROR #none SYSLOG_LEVEL_NONE=0 -_DEBUG_WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh" +NOTIFY_LEVEL_DISABLE=0 +NOTIFY_LEVEL_ERROR=1 +NOTIFY_LEVEL_RENEW=2 +NOTIFY_LEVEL_SKIP=3 -_PREPARE_LINK="https://github.com/Neilpang/acme.sh/wiki/Install-preparations" +NOTIFY_LEVEL_DEFAULT=$NOTIFY_LEVEL_RENEW -_STATELESS_WIKI="https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode" +NOTIFY_MODE_BULK=0 +NOTIFY_MODE_CERT=1 -_DNS_ALIAS_WIKI="https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode" +NOTIFY_MODE_DEFAULT=$NOTIFY_MODE_BULK -_DNS_MANUAL_WIKI="https://github.com/Neilpang/acme.sh/wiki/dns-manual-mode" +_BASE64_ENCODED_CFGS="Le_PreHook Le_PostHook Le_RenewHook Le_Preferred_Chain Le_ReloadCmd" + +_DEBUG_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh" + +_PREPARE_LINK="https://github.com/acmesh-official/acme.sh/wiki/Install-preparations" + +_STATELESS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode" + +_DNS_ALIAS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode" + +_DNS_MANUAL_WIKI="https://github.com/acmesh-official/acme.sh/wiki/dns-manual-mode" + +_DNS_API_WIKI="https://github.com/acmesh-official/acme.sh/wiki/dnsapi" + +_NOTIFY_WIKI="https://github.com/acmesh-official/acme.sh/wiki/notify" + +_SUDO_WIKI="https://github.com/acmesh-official/acme.sh/wiki/sudo" + +_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" + +_VALIDITY_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Validity" + +_DNSCHECK_WIKI="https://github.com/acmesh-official/acme.sh/wiki/dnscheck" _DNS_MANUAL_ERR="The dns manual mode can not renew automatically, you must issue it again manually. You'd better use the other modes instead." @@ -124,26 +192,23 @@ if [ -t 1 ]; then fi __green() { - if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then - printf '\033[1;31;32m' + if [ "${__INTERACTIVE}${ACME_NO_COLOR:-0}" = "10" -o "${ACME_FORCE_COLOR}" = "1" ]; then + printf '\33[1;32m%b\33[0m' "$1" + return fi printf -- "%b" "$1" - if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then - printf '\033[0m' - fi } __red() { - if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then - printf '\033[1;31;40m' + if [ "${__INTERACTIVE}${ACME_NO_COLOR:-0}" = "10" -o "${ACME_FORCE_COLOR}" = "1" ]; then + printf '\33[1;31m%b\33[0m' "$1" + return fi printf -- "%b" "$1" - if [ "$__INTERACTIVE${ACME_NO_COLOR}" = "1" -o "${ACME_FORCE_COLOR}" = "1" ]; then - printf '\033[0m' - fi } _printargs() { + _exitstatus="$?" if [ -z "$NO_TIMESTAMP" ] || [ "$NO_TIMESTAMP" = "0" ]; then printf -- "%s" "[$(date)] " fi @@ -153,6 +218,8 @@ _printargs() { printf -- "%s" "$1='$2'" fi printf "\n" + # return the saved exit status + return "$_exitstatus" } _dlg_versions() { @@ -161,33 +228,34 @@ _dlg_versions() { if _exists "${ACME_OPENSSL_BIN:-openssl}"; then ${ACME_OPENSSL_BIN:-openssl} version 2>&1 else - echo "$ACME_OPENSSL_BIN doesn't exists." + echo "$ACME_OPENSSL_BIN doesn't exist." fi echo "apache:" if [ "$_APACHECTL" ] && _exists "$_APACHECTL"; then $_APACHECTL -V 2>&1 else - echo "apache doesn't exists." + echo "apache doesn't exist." fi echo "nginx:" if _exists "nginx"; then nginx -V 2>&1 else - echo "nginx doesn't exists." + echo "nginx doesn't exist." fi echo "socat:" if _exists "socat"; then - socat -h 2>&1 + socat -V 2>&1 else - _debug "socat doesn't exists." + _debug "socat doesn't exist." fi } #class _syslog() { + _exitstatus="$?" if [ "${SYS_LOG:-$SYSLOG_LEVEL_NONE}" = "$SYSLOG_LEVEL_NONE" ]; then return fi @@ -201,6 +269,7 @@ _syslog() { fi fi $__logger_i -t "$PROJECT_NAME" -p "$_logclass" "$(_printargs "$@")" >/dev/null 2>&1 + return "$_exitstatus" } _log() { @@ -236,6 +305,37 @@ _usage() { printf "\n" >&2 } +__debug_bash_helper() { + # At this point only do for --debug 3 + if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -lt "$DEBUG_LEVEL_3" ]; then + return + fi + # Return extra debug info when running with bash, otherwise return empty + # string. + if [ -z "${BASH_VERSION}" ]; then + return + fi + # We are a bash shell at this point, return the filename, function name, and + # line number as a string + _dbh_saveIFS=$IFS + IFS=" " + # Must use eval or syntax error happens under dash. The eval should use + # single quotes as older versions of busybox had a bug with double quotes and + # eval. + # Use 'caller 1' as we want one level up the stack as we should be called + # by one of the _debug* functions + eval '_dbh_called=($(caller 1))' + IFS=$_dbh_saveIFS + eval '_dbh_file=${_dbh_called[2]}' + if [ -n "${_script_home}" ]; then + # Trim off the _script_home directory name + eval '_dbh_file=${_dbh_file#$_script_home/}' + fi + eval '_dbh_function=${_dbh_called[1]}' + eval '_dbh_lineno=${_dbh_called[0]}' + printf "%-40s " "$_dbh_file:${_dbh_function}:${_dbh_lineno}" +} + _debug() { if [ "${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" -ge "$LOG_LEVEL_1" ]; then _log "$@" @@ -244,7 +344,8 @@ _debug() { _syslog "$SYSLOG_DEBUG" "$@" fi if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_1" ]; then - _printargs "$@" >&2 + _bash_debug=$(__debug_bash_helper) + _printargs "${_bash_debug}$@" >&2 fi } @@ -277,7 +378,8 @@ _debug2() { _syslog "$SYSLOG_DEBUG" "$@" fi if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_2" ]; then - _printargs "$@" >&2 + _bash_debug=$(__debug_bash_helper) + _printargs "${_bash_debug}$@" >&2 fi } @@ -309,7 +411,8 @@ _debug3() { _syslog "$SYSLOG_DEBUG" "$@" fi if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_3" ]; then - _printargs "$@" >&2 + _bash_debug=$(__debug_bash_helper) + _printargs "${_bash_debug}$@" >&2 fi } @@ -335,18 +438,18 @@ _secure_debug3() { _upper_case() { # shellcheck disable=SC2018,SC2019 - tr 'a-z' 'A-Z' + tr '[a-z]' '[A-Z]' } _lower_case() { # shellcheck disable=SC2018,SC2019 - tr 'A-Z' 'a-z' + tr '[A-Z]' '[a-z]' } _startswith() { _str="$1" _sub="$2" - echo "$_str" | grep "^$_sub" >/dev/null 2>&1 + echo "$_str" | grep -- "^$_sub" >/dev/null 2>&1 } _endswith() { @@ -441,27 +544,27 @@ _math() { _h_char_2_dec() { _ch=$1 case "${_ch}" in - a | A) - printf "10" - ;; - b | B) - printf "11" - ;; - c | C) - printf "12" - ;; - d | D) - printf "13" - ;; - e | E) - printf "14" - ;; - f | F) - printf "15" - ;; - *) - printf "%s" "$_ch" - ;; + a | A) + printf "10" + ;; + b | B) + printf "11" + ;; + c | C) + printf "12" + ;; + d | D) + printf "13" + ;; + e | E) + printf "14" + ;; + f | F) + printf "15" + ;; + *) + printf "%s" "$_ch" + ;; esac } @@ -477,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) @@ -569,215 +680,230 @@ _url_encode() { for _hex_code in $_hex_str; do #upper case case "${_hex_code}" in - "41") - printf "%s" "A" - ;; - "42") - printf "%s" "B" - ;; - "43") - printf "%s" "C" - ;; - "44") - printf "%s" "D" - ;; - "45") - printf "%s" "E" - ;; - "46") - printf "%s" "F" - ;; - "47") - printf "%s" "G" - ;; - "48") - printf "%s" "H" - ;; - "49") - printf "%s" "I" - ;; - "4a") - printf "%s" "J" - ;; - "4b") - printf "%s" "K" - ;; - "4c") - printf "%s" "L" - ;; - "4d") - printf "%s" "M" - ;; - "4e") - printf "%s" "N" - ;; - "4f") - printf "%s" "O" - ;; - "50") - printf "%s" "P" - ;; - "51") - printf "%s" "Q" - ;; - "52") - printf "%s" "R" - ;; - "53") - printf "%s" "S" - ;; - "54") - printf "%s" "T" - ;; - "55") - printf "%s" "U" - ;; - "56") - printf "%s" "V" - ;; - "57") - printf "%s" "W" - ;; - "58") - printf "%s" "X" - ;; - "59") - printf "%s" "Y" - ;; - "5a") - printf "%s" "Z" - ;; + "41") + printf "%s" "A" + ;; + "42") + printf "%s" "B" + ;; + "43") + printf "%s" "C" + ;; + "44") + printf "%s" "D" + ;; + "45") + printf "%s" "E" + ;; + "46") + printf "%s" "F" + ;; + "47") + printf "%s" "G" + ;; + "48") + printf "%s" "H" + ;; + "49") + printf "%s" "I" + ;; + "4a") + printf "%s" "J" + ;; + "4b") + printf "%s" "K" + ;; + "4c") + printf "%s" "L" + ;; + "4d") + printf "%s" "M" + ;; + "4e") + printf "%s" "N" + ;; + "4f") + printf "%s" "O" + ;; + "50") + printf "%s" "P" + ;; + "51") + printf "%s" "Q" + ;; + "52") + printf "%s" "R" + ;; + "53") + printf "%s" "S" + ;; + "54") + printf "%s" "T" + ;; + "55") + printf "%s" "U" + ;; + "56") + printf "%s" "V" + ;; + "57") + printf "%s" "W" + ;; + "58") + printf "%s" "X" + ;; + "59") + printf "%s" "Y" + ;; + "5a") + printf "%s" "Z" + ;; #lower case - "61") - printf "%s" "a" - ;; - "62") - printf "%s" "b" - ;; - "63") - printf "%s" "c" - ;; - "64") - printf "%s" "d" - ;; - "65") - printf "%s" "e" - ;; - "66") - printf "%s" "f" - ;; - "67") - printf "%s" "g" - ;; - "68") - printf "%s" "h" - ;; - "69") - printf "%s" "i" - ;; - "6a") - printf "%s" "j" - ;; - "6b") - printf "%s" "k" - ;; - "6c") - printf "%s" "l" - ;; - "6d") - printf "%s" "m" - ;; - "6e") - printf "%s" "n" - ;; - "6f") - printf "%s" "o" - ;; - "70") - printf "%s" "p" - ;; - "71") - printf "%s" "q" - ;; - "72") - printf "%s" "r" - ;; - "73") - printf "%s" "s" - ;; - "74") - printf "%s" "t" - ;; - "75") - printf "%s" "u" - ;; - "76") - printf "%s" "v" - ;; - "77") - printf "%s" "w" - ;; - "78") - printf "%s" "x" - ;; - "79") - printf "%s" "y" - ;; - "7a") - printf "%s" "z" - ;; + "61") + printf "%s" "a" + ;; + "62") + printf "%s" "b" + ;; + "63") + printf "%s" "c" + ;; + "64") + printf "%s" "d" + ;; + "65") + printf "%s" "e" + ;; + "66") + printf "%s" "f" + ;; + "67") + printf "%s" "g" + ;; + "68") + printf "%s" "h" + ;; + "69") + printf "%s" "i" + ;; + "6a") + printf "%s" "j" + ;; + "6b") + printf "%s" "k" + ;; + "6c") + printf "%s" "l" + ;; + "6d") + printf "%s" "m" + ;; + "6e") + printf "%s" "n" + ;; + "6f") + printf "%s" "o" + ;; + "70") + printf "%s" "p" + ;; + "71") + printf "%s" "q" + ;; + "72") + printf "%s" "r" + ;; + "73") + printf "%s" "s" + ;; + "74") + printf "%s" "t" + ;; + "75") + printf "%s" "u" + ;; + "76") + printf "%s" "v" + ;; + "77") + printf "%s" "w" + ;; + "78") + printf "%s" "x" + ;; + "79") + printf "%s" "y" + ;; + "7a") + printf "%s" "z" + ;; #numbers - "30") - printf "%s" "0" - ;; - "31") - printf "%s" "1" - ;; - "32") - printf "%s" "2" - ;; - "33") - printf "%s" "3" - ;; - "34") - printf "%s" "4" - ;; - "35") - printf "%s" "5" - ;; - "36") - printf "%s" "6" - ;; - "37") - printf "%s" "7" - ;; - "38") - printf "%s" "8" - ;; - "39") - printf "%s" "9" - ;; - "2d") - printf "%s" "-" - ;; - "5f") - printf "%s" "_" - ;; - "2e") - printf "%s" "." - ;; - "7e") - printf "%s" "~" - ;; - #other hex - *) - printf '%%%s' "$_hex_code" - ;; + "30") + printf "%s" "0" + ;; + "31") + printf "%s" "1" + ;; + "32") + printf "%s" "2" + ;; + "33") + printf "%s" "3" + ;; + "34") + printf "%s" "4" + ;; + "35") + printf "%s" "5" + ;; + "36") + printf "%s" "6" + ;; + "37") + printf "%s" "7" + ;; + "38") + printf "%s" "8" + ;; + "39") + printf "%s" "9" + ;; + "2d") + printf "%s" "-" + ;; + "5f") + printf "%s" "_" + ;; + "2e") + printf "%s" "." + ;; + "7e") + printf "%s" "~" + ;; + #other hex + *) + printf '%%%s' "$_hex_code" + ;; esac done } +_json_encode() { + _j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")" + _debug3 "_json_encode" + _debug3 "_j_str" "$_j_str" + echo "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n" +} + +#from: http:\/\/ to http:// +_json_decode() { + _j_str="$(sed 's#\\/#/#g')" + _debug3 "_json_decode" + _debug3 "_j_str" "$_j_str" + echo "$_j_str" +} + #options file _sed_i() { options="$1" @@ -797,8 +923,16 @@ _sed_i() { fi } +if [ "$(echo abc | egrep -o b 2>/dev/null)" = "b" ]; then + __USE_EGREP=1 +else + __USE_EGREP="" +fi + _egrep_o() { - if ! egrep -o "$1" 2>/dev/null; then + if [ "$__USE_EGREP" ]; then + egrep -o -- "$1" 2>/dev/null + else sed -n 's/.*\('"$1"'\).*/\1/p' fi } @@ -848,9 +982,9 @@ _base64() { #Usage: multiline _dbase64() { if [ "$1" ]; then - ${ACME_OPENSSL_BIN:-openssl} base64 -d -A - else ${ACME_OPENSSL_BIN:-openssl} base64 -d + else + ${ACME_OPENSSL_BIN:-openssl} base64 -d -A fi } @@ -858,9 +992,9 @@ _dbase64() { _checkcert() { _cf="$1" if [ "$DEBUG" ]; then - openssl x509 -noout -text -in "$_cf" + ${ACME_OPENSSL_BIN:-openssl} x509 -noout -text -in "$_cf" else - openssl x509 -noout -text -in "$_cf" >/dev/null 2>&1 + ${ACME_OPENSSL_BIN:-openssl} x509 -noout -text -in "$_cf" >/dev/null 2>&1 fi } @@ -925,9 +1059,9 @@ _sign() { _sign_openssl="${ACME_OPENSSL_BIN:-openssl} dgst -sign $keyfile " - if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + if _isRSA "$keyfile" >/dev/null 2>&1; then $_sign_openssl -$alg | _base64 - elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + elif _isEcc "$keyfile" >/dev/null 2>&1; then if ! _signedECText="$($_sign_openssl -sha$__ECC_KEY_LEN | ${ACME_OPENSSL_BIN:-openssl} asn1parse -inform DER)"; then _err "Sign failed: $_sign_openssl" _err "Key file: $keyfile" @@ -936,8 +1070,32 @@ _sign() { fi _debug3 "_signedECText" "$_signedECText" _ec_r="$(echo "$_signedECText" | _head_n 2 | _tail_n 1 | cut -d : -f 4 | tr -d "\r\n")" - _debug3 "_ec_r" "$_ec_r" _ec_s="$(echo "$_signedECText" | _head_n 3 | _tail_n 1 | cut -d : -f 4 | tr -d "\r\n")" + if [ "$__ECC_KEY_LEN" -eq "256" ]; then + while [ "${#_ec_r}" -lt "64" ]; do + _ec_r="0${_ec_r}" + done + while [ "${#_ec_s}" -lt "64" ]; do + _ec_s="0${_ec_s}" + done + fi + if [ "$__ECC_KEY_LEN" -eq "384" ]; then + while [ "${#_ec_r}" -lt "96" ]; do + _ec_r="0${_ec_r}" + done + while [ "${#_ec_s}" -lt "96" ]; do + _ec_s="0${_ec_s}" + done + fi + if [ "$__ECC_KEY_LEN" -eq "512" ]; then + while [ "${#_ec_r}" -lt "132" ]; do + _ec_r="0${_ec_r}" + done + while [ "${#_ec_s}" -lt "132" ]; do + _ec_s="0${_ec_s}" + done + fi + _debug3 "_ec_r" "$_ec_r" _debug3 "_ec_s" "$_ec_s" printf "%s" "$_ec_r$_ec_s" | _h2b | _base64 else @@ -955,11 +1113,11 @@ _isEccKey() { return 1 fi - [ "$_length" != "1024" ] \ - && [ "$_length" != "2048" ] \ - && [ "$_length" != "3072" ] \ - && [ "$_length" != "4096" ] \ - && [ "$_length" != "8192" ] + [ "$_length" != "1024" ] && + [ "$_length" != "2048" ] && + [ "$_length" != "3072" ] && + [ "$_length" != "4096" ] && + [ "$_length" != "8192" ] } # _createkey 2048|ec-256 file @@ -989,21 +1147,41 @@ _createkey() { _debug "Use length $length" - if ! touch "$f" >/dev/null 2>&1; then - _f_path="$(dirname "$f")" - _debug _f_path "$_f_path" - if ! mkdir -p "$_f_path"; then - _err "Can not create path: $_f_path" + if ! [ -e "$f" ]; then + if ! touch "$f" >/dev/null 2>&1; then + _f_path="$(dirname "$f")" + _debug _f_path "$_f_path" + if ! mkdir -p "$_f_path"; then + _err "Can not create path: $_f_path" + return 1 + fi + fi + if ! touch "$f" >/dev/null 2>&1; then return 1 fi + chmod 600 "$f" fi if _isEccKey "$length"; then _debug "Using ec name: $eccname" - ${ACME_OPENSSL_BIN:-openssl} ecparam -name "$eccname" -genkey 2>/dev/null >"$f" + 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" + return 1 + fi else _debug "Using RSA: $length" - ${ACME_OPENSSL_BIN:-openssl} genrsa "$length" 2>/dev/null >"$f" + __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" + return 1 + fi fi if [ "$?" != "0" ]; then @@ -1016,7 +1194,7 @@ _createkey() { _is_idn() { _is_idn_d="$1" _debug2 _is_idn_d "$_is_idn_d" - _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '0-9' | tr -d 'a-z' | tr -d 'A-Z' | tr -d '*.,-') + _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '[0-9]' | tr -d '[a-z]' | tr -d '[A-Z]' | tr -d '*.,-_') _debug2 _idn_temp "$_idn_temp" [ "$_idn_temp" ] } @@ -1050,7 +1228,7 @@ _idn() { fi } -#_createcsr cn san_list keyfile csrfile conf +#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 _createcsr() { _debug _createcsr domain="$1" @@ -1058,42 +1236,66 @@ _createcsr() { csrkey="$3" csr="$4" csrconf="$5" + acmeValidationv1="$6" _debug2 domain "$domain" _debug2 domainlist "$domainlist" _debug2 csrkey "$csrkey" _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 ]\nextendedKeyUsage=serverAuth,clientAuth\n" >"$csrconf" - if [ -z "$domainlist" ] || [ "$domainlist" = "$NO_VALUE" ]; then + if [ "$acmeValidationv1" ]; then + domainlist="$(_idn "$domainlist")" + _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:$domain" >>"$csrconf" + printf -- "\nsubjectAltName=$(_getIdType "$domain" | _upper_case):$(_idn "$domain")" >>"$csrconf" else domainlist="$(_idn "$domainlist")" _debug2 domainlist "$domainlist" - if _contains "$domainlist" ","; then - alt="DNS:$domain,DNS:$(echo "$domainlist" | sed "s/,,/,/g" | sed "s/,/,DNS:/g")" - else - alt="DNS:$domain,DNS:$domainlist" - fi + alt="$(_getIdType "$domain" | _upper_case):$(_idn "$domain")" + for dl in $(echo "'$domainlist'" | sed "s/,/' '/g"); do + dl=$(echo "$dl" | tr -d "'") + alt="$alt,$(_getIdType "$dl" | _upper_case):$dl" + done #multi _info "Multi domain" "$alt" printf -- "\nsubjectAltName=$alt" >>"$csrconf" fi - if [ "$Le_OCSP_Staple" ] || [ "$Le_OCSP_Stable" ]; then + if [ "$Le_OCSP_Staple" = "1" ]; then _savedomainconf Le_OCSP_Staple "$Le_OCSP_Staple" - _cleardomainconf Le_OCSP_Stable printf -- "\nbasicConstraints = CA:FALSE\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >>"$csrconf" fi + if [ "$acmeValidationv1" ]; then + printf "\n1.3.6.1.5.5.7.1.31=critical,DER:04:20:${acmeValidationv1}" >>"${csrconf}" + fi + _csr_cn="$(_idn "$domain")" _debug2 _csr_cn "$_csr_cn" if _contains "$(uname -a)" "MINGW"; then - ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//CN=$_csr_cn" -config "$csrconf" -out "$csr" + if _isIP "$_csr_cn"; then + ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//O=$PROJECT_NAME" -config "$csrconf" -out "$csr" + else + ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//CN=$_csr_cn" -config "$csrconf" -out "$csr" + fi else - ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr" + if _isIP "$_csr_cn"; then + ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/O=$PROJECT_NAME" -config "$csrconf" -out "$csr" + else + ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr" + fi fi } @@ -1138,12 +1340,17 @@ _readSubjectAltNamesFromCSR() { if _contains "$_dnsAltnames," "DNS:$_csrsubj,"; then _debug "AltNames contains subject" - _dnsAltnames="$(printf "%s" "$_dnsAltnames," | sed "s/DNS:$_csrsubj,//g")" + _excapedAlgnames="$(echo "$_dnsAltnames" | tr '*' '#')" + _debug _excapedAlgnames "$_excapedAlgnames" + _escapedSubject="$(echo "$_csrsubj" | tr '*' '#')" + _debug _escapedSubject "$_escapedSubject" + _dnsAltnames="$(echo "$_excapedAlgnames," | sed "s/DNS:$_escapedSubject,//g" | tr '#' '*' | sed "s/,\$//g")" + _debug _dnsAltnames "$_dnsAltnames" else _debug "AltNames doesn't contain subject" fi - printf "%s" "$_dnsAltnames" | sed "s/DNS://g" + echo "$_dnsAltnames" | sed "s/DNS://g" } #_csrfile @@ -1181,7 +1388,7 @@ _ss() { if _exists "netstat"; then _debug "Using: netstat" - if netstat -h 2>&1 | grep "\-p proto" >/dev/null; then + if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then #for windows version netstat tool netstat -an -p tcp | grep "LISTENING" | grep ":$_port " else @@ -1231,7 +1438,7 @@ toPkcs() { domain="$1" pfxPassword="$2" if [ -z "$domain" ]; then - _usage "Usage: $PROJECT_ENTRY --toPkcs -d domain [--password pfx-password]" + _usage "Usage: $PROJECT_ENTRY --to-pkcs12 --domain [--password ] [--ecc]" return 1 fi @@ -1252,7 +1459,7 @@ toPkcs8() { domain="$1" if [ -z "$domain" ]; then - _usage "Usage: $PROJECT_ENTRY --toPkcs8 -d domain [--ecc]" + _usage "Usage: $PROJECT_ENTRY --to-pkcs8 --domain [--ecc]" return 1 fi @@ -1272,7 +1479,7 @@ toPkcs8() { createAccountKey() { _info "Creating account key" if [ -z "$1" ]; then - _usage "Usage: $PROJECT_ENTRY --createAccountKey --accountkeylength 2048" + _usage "Usage: $PROJECT_ENTRY --create-account-key [--accountkeylength ]" return fi @@ -1294,13 +1501,18 @@ _create_account_key() { _initpath mkdir -p "$CA_DIR" - if [ -f "$ACCOUNT_KEY_PATH" ]; then + if [ -s "$ACCOUNT_KEY_PATH" ]; then _info "Account key exists, skip" - return + return 0 else #generate account key - _createkey "$length" "$ACCOUNT_KEY_PATH" - chmod 600 "$ACCOUNT_KEY_PATH" + if _createkey "$length" "$ACCOUNT_KEY_PATH"; then + _info "Create account key ok." + return 0 + else + _err "Create account key error." + return 1 + fi fi } @@ -1309,7 +1521,7 @@ _create_account_key() { createDomainKey() { _info "Creating domain key" if [ -z "$1" ]; then - _usage "Usage: $PROJECT_ENTRY --createDomainKey -d domain.com [ --keylength 2048 ]" + _usage "Usage: $PROJECT_ENTRY --create-domain-key --domain [--keylength ]" return fi @@ -1323,14 +1535,17 @@ createDomainKey() { _initpath "$domain" "$_cdl" - if [ ! -f "$CERT_KEY_PATH" ] || ([ "$FORCE" ] && ! [ "$IS_RENEW" ]) || [ "$Le_ForceNewDomainKey" = "1" ]; then + if [ ! -f "$CERT_KEY_PATH" ] || [ ! -s "$CERT_KEY_PATH" ] || ([ "$FORCE" ] && ! [ "$_ACME_IS_RENEW" ]) || [ "$Le_ForceNewDomainKey" = "1" ]; then if _createkey "$_cdl" "$CERT_KEY_PATH"; then _savedomainconf Le_Keylength "$_cdl" _info "The domain key is here: $(__green $CERT_KEY_PATH)" return 0 + else + _err "Can not create domain key" + return 1 fi else - if [ "$IS_RENEW" ]; then + if [ "$_ACME_IS_RENEW" ]; then _info "Domain key exists, skip" return 0 else @@ -1346,7 +1561,7 @@ createDomainKey() { createCSR() { _info "Creating csr" if [ -z "$1" ]; then - _usage "Usage: $PROJECT_ENTRY --createCSR -d domain1.com [-d domain2.com -d domain3.com ... ]" + _usage "Usage: $PROJECT_ENTRY --create-csr --domain [--domain ...] [--ecc]" return fi @@ -1356,7 +1571,7 @@ createCSR() { _initpath "$domain" "$_isEcc" - if [ -f "$CSR_PATH" ] && [ "$IS_RENEW" ] && [ -z "$FORCE" ]; then + if [ -f "$CSR_PATH" ] && [ "$_ACME_IS_RENEW" ] && [ -z "$FORCE" ]; then _info "CSR exists, skip" return fi @@ -1374,25 +1589,37 @@ _url_replace() { tr '/+' '_-' | tr -d '= ' } +#base64 string +_durl_replace_base64() { + _l=$((${#1} % 4)) + if [ $_l -eq 2 ]; then + _s="$1"'==' + elif [ $_l -eq 3 ]; then + _s="$1"'=' + else + _s="$1" + fi + echo "$_s" | tr '_-' '/+' +} + _time2str() { #BSD - if date -u -r "$1" 2>/dev/null; then + if date -u -r "$1" -j "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null; then return fi #Linux - if date -u -d@"$1" 2>/dev/null; then + if date -u --date=@"$1" "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null; then return fi #Solaris - if _exists adb; then - _t_s_a=$(echo "0t${1}=Y" | adb) - echo "$_t_s_a" + if printf "%(%Y-%m-%dT%H:%M:%SZ)T\n" $1 2>/dev/null; then + return fi #Busybox - if echo "$1" | awk '{ print strftime("%c", $0); }' 2>/dev/null; then + if echo "$1" | awk '{ print strftime("%Y-%m-%dT%H:%M:%SZ", $0); }' 2>/dev/null; then return fi } @@ -1415,6 +1642,24 @@ _stat() { return 1 #error, 'stat' not found } +#keyfile +_isRSA() { + keyfile=$1 + if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1 || ${ACME_OPENSSL_BIN:-openssl} rsa -in "$keyfile" -noout -text 2>&1 | grep "^publicExponent:" 2>&1 >/dev/null; then + return 0 + fi + return 1 +} + +#keyfile +_isEcc() { + keyfile=$1 + if grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1 || ${ACME_OPENSSL_BIN:-openssl} ec -in "$keyfile" -noout -text 2>/dev/null | grep "^NIST CURVE:" 2>&1 >/dev/null; then + return 0 + fi + return 1 +} + #keyfile _calcjwk() { keyfile="$1" @@ -1428,7 +1673,7 @@ _calcjwk() { return 0 fi - if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + if _isRSA "$keyfile"; then _debug "RSA key" pub_exp=$(${ACME_OPENSSL_BIN:-openssl} rsa -in "$keyfile" -noout -text | grep "^publicExponent:" | cut -d '(' -f 2 | cut -d 'x' -f 2 | cut -d ')' -f 1) if [ "${#pub_exp}" = "5" ]; then @@ -1450,7 +1695,7 @@ _calcjwk() { JWK_HEADER='{"alg": "RS256", "jwk": '$jwk'}' JWK_HEADERPLACE_PART1='{"nonce": "' JWK_HEADERPLACE_PART2='", "alg": "RS256"' - elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then + elif _isEcc "$keyfile"; then _debug "EC key" crv="$(${ACME_OPENSSL_BIN:-openssl} ec -in "$keyfile" -noout -text 2>/dev/null | grep "^NIST CURVE:" | cut -d ":" -f 2 | tr -d " \r\n")" _debug3 crv "$crv" @@ -1464,22 +1709,22 @@ _calcjwk() { crv_oid="$(${ACME_OPENSSL_BIN:-openssl} ec -in "$keyfile" -noout -text 2>/dev/null | grep "^ASN1 OID:" | cut -d ":" -f 2 | tr -d " \r\n")" _debug3 crv_oid "$crv_oid" case "${crv_oid}" in - "prime256v1") - crv="P-256" - __ECC_KEY_LEN=256 - ;; - "secp384r1") - crv="P-384" - __ECC_KEY_LEN=384 - ;; - "secp521r1") - crv="P-521" - __ECC_KEY_LEN=512 - ;; - *) - _err "ECC oid : $crv_oid" - return 1 - ;; + "prime256v1") + crv="P-256" + __ECC_KEY_LEN=256 + ;; + "secp384r1") + crv="P-384" + __ECC_KEY_LEN=384 + ;; + "secp521r1") + crv="P-521" + __ECC_KEY_LEN=512 + ;; + *) + _err "ECC oid : $crv_oid" + return 1 + ;; esac _debug3 crv "$crv" fi @@ -1507,7 +1752,7 @@ _calcjwk() { _debug3 x64 "$x64" xend=$(_math "$xend" + 1) - y="$(printf "%s" "$pubtext" | cut -d : -f "$xend"-10000)" + y="$(printf "%s" "$pubtext" | cut -d : -f "$xend"-2048)" _debug3 y "$y" y64="$(printf "%s" "$y" | tr -d : | _h2b | _base64 | _url_replace)" @@ -1520,7 +1765,8 @@ _calcjwk() { JWK_HEADERPLACE_PART1='{"nonce": "' JWK_HEADERPLACE_PART2='", "alg": "ES'$__ECC_KEY_LEN'"' else - _err "Only RSA or EC key is supported." + _err "Only RSA or EC key is supported. keyfile=$keyfile" + _debug2 "$(cat "$keyfile")" return 1 fi @@ -1532,6 +1778,31 @@ _time() { date -u "+%s" } +#support 2 formats: +# 2022-04-01 08:10:33 to 1648800633 +#or 2022-04-01T08:10:33Z to 1648800633 +_date2time() { + #Linux + if date -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then + return + fi + + #Solaris + if gdate -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then + return + fi + #Mac/BSD + if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then + return + fi + #Omnios + if da="$(echo "$1" | tr -d "Z" | tr "T" ' ')" perl -MTime::Piece -e 'print Time::Piece->strptime($ENV{da}, "%Y-%m-%d %H:%M:%S")->epoch, "\n";' 2>/dev/null; then + return + fi + _err "Can not parse _date2time $1" + return 1 +} + _utc_date() { date -u "+%Y-%m-%d %H:%M:%S" } @@ -1555,6 +1826,14 @@ _mktemp() { _err "Can not create temp file." } +#clear all the https envs to cause _inithttp() to run next time. +_resethttp() { + __HTTP_INITIALIZED="" + _ACME_CURL="" + _ACME_WGET="" + ACME_HTTP_NO_REDIRECTS="" +} + _inithttp() { if [ -z "$HTTP_HEADER" ] || ! touch "$HTTP_HEADER"; then @@ -1570,8 +1849,11 @@ _inithttp() { fi if [ -z "$_ACME_CURL" ] && _exists "curl"; then - _ACME_CURL="curl -L --silent --dump-header $HTTP_HEADER " - if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _ACME_CURL="curl --silent --dump-header $HTTP_HEADER " + if [ -z "$ACME_HTTP_NO_REDIRECTS" ]; then + _ACME_CURL="$_ACME_CURL -L " + fi + if [ "$DEBUG" ] && [ "$DEBUG" -ge 2 ]; then _CURL_DUMP="$(_mktemp)" _ACME_CURL="$_ACME_CURL --trace-ascii $_CURL_DUMP " fi @@ -1582,26 +1864,37 @@ _inithttp() { _ACME_CURL="$_ACME_CURL --cacert $CA_BUNDLE " fi - if _contains "$(curl --help 2>&1)" "--globoff"; then + if _contains "$(curl --help 2>&1)" "--globoff" || _contains "$(curl --help curl 2>&1)" "--globoff"; then _ACME_CURL="$_ACME_CURL -g " fi + + #don't use --fail-with-body + ##from curl 7.76: return fail on HTTP errors but keep the body + #if _contains "$(curl --help http 2>&1)" "--fail-with-body"; then + # _ACME_CURL="$_ACME_CURL --fail-with-body " + #fi fi if [ -z "$_ACME_WGET" ] && _exists "wget"; then _ACME_WGET="wget -q" + if [ "$ACME_HTTP_NO_REDIRECTS" ]; then + _ACME_WGET="$_ACME_WGET --max-redirect 0 " + fi if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then - _ACME_WGET="$_ACME_WGET -d " + if [ "$_ACME_WGET" ] && _contains "$($_ACME_WGET --help 2>&1)" "--debug"; then + _ACME_WGET="$_ACME_WGET -d " + fi fi if [ "$CA_PATH" ]; then _ACME_WGET="$_ACME_WGET --ca-directory=$CA_PATH " elif [ "$CA_BUNDLE" ]; then _ACME_WGET="$_ACME_WGET --ca-certificate=$CA_BUNDLE " fi - fi - #from wget 1.14: do not skip body on 404 error - if [ "$_ACME_WGET" ] && _contains "$($_ACME_WGET --help 2>&1)" "--content-on-error"; then - _ACME_WGET="$_ACME_WGET --content-on-error " + #from wget 1.14: do not skip body on 404 error + if _contains "$(wget --help 2>&1)" "--content-on-error"; then + _ACME_WGET="$_ACME_WGET --content-on-error " + fi fi __HTTP_INITIALIZED=1 @@ -1631,18 +1924,37 @@ _post() { if [ "$HTTPS_INSECURE" ]; then _CURL="$_CURL --insecure " fi + if [ "$httpmethod" = "HEAD" ]; then + _CURL="$_CURL -I " + fi _debug "_CURL" "$_CURL" if [ "$needbase64" ]; then - if [ "$_postContentType" ]; then - response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)" + if [ "$body" ]; then + if [ "$_postContentType" ]; then + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)" + else + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)" + fi else - response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)" + if [ "$_postContentType" ]; then + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url" | _base64)" + else + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url" | _base64)" + fi fi else - if [ "$_postContentType" ]; then - response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")" + if [ "$body" ]; then + if [ "$_postContentType" ]; then + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")" + else + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")" + fi else - response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")" + if [ "$_postContentType" ]; then + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url")" + else + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url")" + fi fi fi _ret="$?" @@ -1658,6 +1970,9 @@ _post() { if [ "$HTTPS_INSECURE" ]; then _WGET="$_WGET --no-check-certificate " fi + if [ "$httpmethod" = "HEAD" ]; then + _WGET="$_WGET --read-timeout=3.0 --tries=2 " + fi _debug "_WGET" "$_WGET" if [ "$needbase64" ]; then if [ "$httpmethod" = "POST" ]; then @@ -1680,6 +1995,12 @@ _post() { else response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" fi + elif [ "$httpmethod" = "HEAD" ]; then + if [ "$_postContentType" ]; then + response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" + else + response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")" + fi else if [ "$_postContentType" ]; then response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --method $httpmethod --body-data="$body" "$_post_url" 2>"$HTTP_HEADER")" @@ -1696,7 +2017,13 @@ _post() { if [ "$_ret" != "0" ]; then _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret" fi - _sed_i "s/^ *//g" "$HTTP_HEADER" + if _contains "$_WGET" " -d "; then + # Demultiplex wget debug output + cat "$HTTP_HEADER" >&2 + _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" + fi + # remove leading whitespaces from header to match curl format + _sed_i 's/^ //g' "$HTTP_HEADER" else _ret="$?" _err "Neither curl nor wget is found, can not do $httpmethod." @@ -1749,9 +2076,21 @@ _get() { fi _debug "_WGET" "$_WGET" if [ "$onlyheader" ]; then - $_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -S -O /dev/null "$url" 2>&1 | sed 's/^[ ]*//g' + _wget_out="$($_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -S -O /dev/null "$url" 2>&1)" + if _contains "$_WGET" " -d "; then + # Demultiplex wget debug output + echo "$_wget_out" >&2 + echo "$_wget_out" | sed '/^[^ ][^ ]/d; /^ *$/d; s/^ //g' - + fi else - $_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -O - "$url" + $_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -S -O - "$url" 2>"$HTTP_HEADER" + if _contains "$_WGET" " -d "; then + # Demultiplex wget debug output + cat "$HTTP_HEADER" >&2 + _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" + fi + # remove leading whitespaces from header to match curl format + _sed_i 's/^ //g' "$HTTP_HEADER" fi ret=$? if [ "$ret" = "8" ]; then @@ -1774,12 +2113,18 @@ _head_n() { } _tail_n() { - if ! tail -n "$1" 2>/dev/null; then + if _is_solaris; then #fix for solaris tail -"$1" + else + tail -n "$1" fi } +_tail_c() { + tail -c "$1" 2>/dev/null || tail -"$1"c +} + # url payload needbase64 keyfile _send_signed_request() { url=$1 @@ -1789,6 +2134,7 @@ _send_signed_request() { if [ -z "$keyfile" ]; then keyfile="$ACCOUNT_KEY_PATH" fi + _debug "=======Begin Send Signed Request=======" _debug url "$url" _debug payload "$payload" @@ -1812,25 +2158,31 @@ _send_signed_request() { if [ "$ACME_NEW_NONCE" ]; then _debug2 "Get nonce with HEAD. ACME_NEW_NONCE" "$ACME_NEW_NONCE" nonceurl="$ACME_NEW_NONCE" - if _post "" "$nonceurl" "" "HEAD" "$__request_conent_type"; then + 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 | cut -d , -f 1)" fi fi - if [ -z "$_headers" ]; then + if [ -z "$_CACHED_NONCE" ]; then _debug2 "Get nonce with GET. ACME_DIRECTORY" "$ACME_DIRECTORY" nonceurl="$ACME_DIRECTORY" _headers="$(_get "$nonceurl" "onlyheader")" + _debug2 _headers "$_headers" + _CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" fi - + if [ -z "$_CACHED_NONCE" ] && [ "$ACME_NEW_NONCE" ]; then + _debug2 "Get nonce with GET. ACME_NEW_NONCE" "$ACME_NEW_NONCE" + nonceurl="$ACME_NEW_NONCE" + _headers="$(_get "$nonceurl" "onlyheader")" + _debug2 _headers "$_headers" + _CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + fi + _debug2 _CACHED_NONCE "$_CACHED_NONCE" if [ "$?" != "0" ]; then _err "Can not connect to $nonceurl to get nonce." return 1 fi - - _debug2 _headers "$_headers" - - _CACHED_NONCE="$(echo "$_headers" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" - _debug2 _CACHED_NONCE "$_CACHED_NONCE" else _debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE" fi @@ -1841,15 +2193,15 @@ _send_signed_request() { _sleep 2 continue fi - if [ "$ACME_VERSION" = "2" ]; then - if [ "$url" = "$ACME_NEW_ACCOUNT" ] || [ "$url" = "$ACME_REVOKE_CERT" ]; 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)" @@ -1864,11 +2216,7 @@ _send_signed_request() { sig="$(printf "%s" "$_sig_t" | _url_replace)" _debug3 sig "$sig" - if [ "$ACME_VERSION" = "2" ]; then - body="{\"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" - else - body="{\"header\": $JWK_HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" - fi + body="{\"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" _debug3 body "$body" response="$(_post "$body" "$url" "$needbase64" "POST" "$__request_conent_type")" @@ -1878,32 +2226,59 @@ _send_signed_request() { _err "Can not post to $url" return 1 fi - _debug2 original "$response" - response="$(echo "$response" | _normalizeJson)" responseHeaders="$(cat "$HTTP_HEADER")" - _debug2 responseHeaders "$responseHeaders" - _debug2 response "$response" + code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" _debug code "$code" - _CACHED_NONCE="$(echo "$responseHeaders" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" - - _body="$response" - if [ "$needbase64" ]; then - _body="$(echo "$_body" | _dbase64 | tr -d '\0')" - _debug3 _body "$_body" + _debug2 original "$response" + if echo "$responseHeaders" | grep -i "Content-Type: *application/json" >/dev/null 2>&1; then + response="$(echo "$response" | _json_decode | _normalizeJson)" fi + _debug2 response "$response" - if _contains "$_body" "JWS has invalid anti-replay nonce" || _contains "$_body" "JWS has an invalid anti-replay nonce"; then - _info "It seems the CA server is busy now, let's wait and retry. Sleeping $_sleep_retry_sec seconds." - _CACHED_NONCE="" - _sleep $_sleep_retry_sec - continue + _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" + if [ "$needbase64" ]; then + _body="$(echo "$_body" | _dbase64 multiline)" + _debug3 _body "$_body" + fi + + _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *: *[0-9]\+ *" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') + if [ "$code" = '503' ]; then + _sleep_overload_retry_sec=$_retryafter + if [ -z "$_sleep_overload_retry_sec" ]; then + _sleep_overload_retry_sec=5 + fi + if [ $_sleep_overload_retry_sec -le 600 ]; then + _info "It seems the CA server is currently overloaded, let's wait and retry. Sleeping $_sleep_overload_retry_sec seconds." + _sleep $_sleep_overload_retry_sec + continue + else + _info "The retryafter=$_retryafter is too large > 600, not retry anymore." + fi + fi + if _contains "$_body" "JWS has invalid anti-replay nonce" || _contains "$_body" "JWS has an invalid anti-replay nonce"; then + _info "It seems the CA server is busy now, let's wait and retry. Sleeping $_sleep_retry_sec seconds." + _CACHED_NONCE="" + _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 - break + return 0 done + _info "Giving up sending to CA server after $MAX_REQUEST_RETRY_TIMES retries." + return 1 } @@ -1921,12 +2296,18 @@ _setopt() { if [ ! -f "$__conf" ]; then touch "$__conf" fi + if [ -n "$(_tail_c 1 <"$__conf")" ]; then + echo >>"$__conf" + fi if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then _debug3 OK if _contains "$__val" "&"; then __val="$(echo "$__val" | sed 's/&/\\&/g')" fi + if _contains "$__val" "|"; then + __val="$(echo "$__val" | sed 's/|/\\|/g')" + fi text="$(cat "$__conf")" printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" @@ -1934,6 +2315,9 @@ _setopt() { if _contains "$__val" "&"; then __val="$(echo "$__val" | sed 's/&/\\&/g')" fi + if _contains "$__val" "|"; then + __val="$(echo "$__val" | sed 's/|/\\|/g')" + fi text="$(cat "$__conf")" printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" @@ -1944,12 +2328,16 @@ _setopt() { _debug3 "$(grep -n "^$__opt$__sep" "$__conf")" } -#_save_conf file key value +#_save_conf file key value base64encode #save to conf _save_conf() { _s_c_f="$1" _sdkey="$2" _sdvalue="$3" + _b64encode="$4" + if [ "$_sdvalue" ] && [ "$_b64encode" ]; then + _sdvalue="${B64CONF_START}$(printf "%s" "${_sdvalue}" | _base64)${B64CONF_END}" + fi if [ "$_s_c_f" ]; then _setopt "$_s_c_f" "$_sdkey" "=" "'$_sdvalue'" else @@ -1974,19 +2362,23 @@ _read_conf() { _r_c_f="$1" _sdkey="$2" if [ -f "$_r_c_f" ]; then - ( + _sdv="$( eval "$(grep "^$_sdkey *=" "$_r_c_f")" eval "printf \"%s\" \"\$$_sdkey\"" - ) + )" + if _startswith "$_sdv" "${B64CONF_START}" && _endswith "$_sdv" "${B64CONF_END}"; then + _sdv="$(echo "$_sdv" | sed "s/${B64CONF_START}//" | sed "s/${B64CONF_END}//" | _dbase64)" + fi + printf "%s" "$_sdv" else _debug "config file is empty, can not read $_sdkey" fi } -#_savedomainconf key value +#_savedomainconf key value base64encode #save to domain.conf _savedomainconf() { - _save_conf "$DOMAIN_CONF" "$1" "$2" + _save_conf "$DOMAIN_CONF" "$@" } #_cleardomainconf key @@ -1999,14 +2391,58 @@ _readdomainconf() { _read_conf "$DOMAIN_CONF" "$1" } -#_saveaccountconf key value -_saveaccountconf() { - _save_conf "$ACCOUNT_CONF_PATH" "$1" "$2" +#_migratedomainconf oldkey newkey base64encode +_migratedomainconf() { + _old_key="$1" + _new_key="$2" + _b64encode="$3" + _value=$(_readdomainconf "$_old_key") + if [ -z "$_value" ]; then + return 1 # oldkey is not found + fi + _savedomainconf "$_new_key" "$_value" "$_b64encode" + _cleardomainconf "$_old_key" + _debug "Domain config $_old_key has been migrated to $_new_key" } -#key value +#_migratedeployconf oldkey newkey base64encode +_migratedeployconf() { + _migratedomainconf "$1" "SAVED_$2" "$3" || + _migratedomainconf "SAVED_$1" "SAVED_$2" "$3" # try only when oldkey itself is not found +} + +#key value base64encode +_savedeployconf() { + _savedomainconf "SAVED_$1" "$2" "$3" + #remove later + _cleardomainconf "$1" +} + +#key +_getdeployconf() { + _rac_key="$1" + _rac_value="$(eval echo \$"$_rac_key")" + if [ "$_rac_value" ]; then + if _startswith "$_rac_value" '"' && _endswith "$_rac_value" '"'; then + _debug2 "trim quotation marks" + eval $_rac_key=$_rac_value + export $_rac_key + fi + return 0 # do nothing + fi + _saved="$(_readdomainconf "SAVED_$_rac_key")" + eval $_rac_key=\$_saved + export $_rac_key +} + +#_saveaccountconf key value base64encode +_saveaccountconf() { + _save_conf "$ACCOUNT_CONF_PATH" "$@" +} + +#key value base64encode _saveaccountconf_mutable() { - _save_conf "$ACCOUNT_CONF_PATH" "SAVED_$1" "$2" + _save_conf "$ACCOUNT_CONF_PATH" "SAVED_$1" "$2" "$3" #remove later _clearaccountconf "$1" } @@ -2027,6 +2463,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" @@ -2046,6 +2489,7 @@ _clearcaconf() { _startserver() { content="$1" ncaddr="$2" + _debug "content" "$content" _debug "ncaddr" "$ncaddr" _debug "startserver: $$" @@ -2072,8 +2516,14 @@ _startserver() { SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}" fi + _content_len="$(printf "%s" "$content" | wc -c)" + _debug _content_len "$_content_len" _debug "_NC" "$_NC $SOCAT_OPTIONS" - $_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; echo HTTP/1.0 200 OK; echo ; echo $content; echo;" & + $_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \ +echo 'HTTP/1.0 200 OK'; \ +echo 'Content-Length\: $_content_len'; \ +echo ''; \ +printf '%s' '$content';" & serverproc="$!" } @@ -2105,7 +2555,7 @@ _sleep() { fi } -# _starttlsserver san_a san_b port content _ncaddr +# _starttlsserver san_a san_b port content _ncaddr acmeValidationv1 _starttlsserver() { _info "Starting tls server." san_a="$1" @@ -2113,10 +2563,12 @@ _starttlsserver() { port="$3" content="$4" opaddr="$5" + acmeValidationv1="$6" _debug san_a "$san_a" _debug san_b "$san_b" _debug port "$port" + _debug acmeValidationv1 "$acmeValidationv1" #create key TLS_KEY if ! _createkey "2048" "$TLS_KEY"; then @@ -2129,7 +2581,7 @@ _starttlsserver() { if [ "$san_b" ]; then alt="$alt,$san_b" fi - if ! _createcsr "tls.acme.sh" "$alt" "$TLS_KEY" "$TLS_CSR" "$TLS_CONF"; then + if ! _createcsr "tls.acme.sh" "$alt" "$TLS_KEY" "$TLS_CSR" "$TLS_CONF" "$acmeValidationv1"; then _err "Create tls validation csr error." return 1 fi @@ -2155,6 +2607,10 @@ _starttlsserver() { __S_OPENSSL="$__S_OPENSSL -6" fi + if [ "$acmeValidationv1" ]; then + __S_OPENSSL="$__S_OPENSSL -alpn acme-tls/1" + fi + _debug "$__S_OPENSSL" if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then $__S_OPENSSL -tlsextdebug & @@ -2193,7 +2649,7 @@ __initHome() { _script_home="$(dirname "$_script")" _debug "_script_home" "$_script_home" if [ -d "$_script_home" ]; then - _SCRIPT_HOME="$_script_home" + export _SCRIPT_HOME="$_script_home" else _err "It seems the script home is not correct:$_script_home" fi @@ -2232,7 +2688,7 @@ __initHome() { if [ -z "$ACCOUNT_CONF_PATH" ]; then ACCOUNT_CONF_PATH="$_DEFAULT_ACCOUNT_CONF_PATH" fi - + _debug3 ACCOUNT_CONF_PATH "$ACCOUNT_CONF_PATH" DEFAULT_LOG_FILE="$LE_CONFIG_HOME/$PROJECT_NAME.log" DEFAULT_CA_HOME="$LE_CONFIG_HOME/ca" @@ -2242,75 +2698,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" @@ -2320,9 +2757,23 @@ _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 +} + +_clearCA() { + export CA_CONF= + export ACCOUNT_KEY_PATH= + export ACCOUNT_JSON_PATH= } #[domain] [keylength or isEcc flag] @@ -2336,7 +2787,7 @@ _initpath() { . "$ACCOUNT_CONF_PATH" fi - if [ "$IN_CRON" ]; then + if [ "$_ACME_IN_CRON" ]; then if [ ! "$_USER_PATH_EXPORTED" ]; then _USER_PATH_EXPORTED=1 export PATH="$USER_PATH:$PATH" @@ -2347,17 +2798,18 @@ _initpath() { CA_HOME="$DEFAULT_CA_HOME" fi - if [ "$ACME_VERSION" = "2" ]; then - DEFAULT_CA="$LETSENCRYPT_CA_V2" - DEFAULT_STAGING_CA="$LETSENCRYPT_STAGING_CA_V2" - fi - if [ -z "$ACME_DIRECTORY" ]; then - if [ -z "$STAGE" ]; then - ACME_DIRECTORY="$DEFAULT_CA" - else + if [ "$STAGE" ]; then ACME_DIRECTORY="$DEFAULT_STAGING_CA" - _info "Using stage ACME_DIRECTORY: $ACME_DIRECTORY" + _info "Using ACME_DIRECTORY: $ACME_DIRECTORY" + else + default_acme_server=$(_readaccountconf "DEFAULT_ACME_SERVER") + _debug default_acme_server "$default_acme_server" + if [ "$default_acme_server" ]; then + ACME_DIRECTORY="$default_acme_server" + else + ACME_DIRECTORY="$DEFAULT_CA" + fi fi fi @@ -2365,15 +2817,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 @@ -2394,19 +2875,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" @@ -2428,12 +2896,14 @@ _initpath() { if _isEccKey "$_ilength"; then DOMAIN_PATH="$domainhomeecc" - else + elif [ -z "$__SELECTED_RSA_KEY" ]; then if [ ! -d "$domainhome" ] && [ -d "$domainhomeecc" ]; then - _info "The domain '$domain' seems to have a ECC cert already, please add '$(__red "--ecc")' parameter if you want to use that cert." + _info "The domain '$domain' seems to have a ECC cert already, lets use ecc cert." + DOMAIN_PATH="$domainhomeecc" fi fi _debug DOMAIN_PATH "$DOMAIN_PATH" + export DOMAIN_PATH fi if [ -z "$DOMAIN_BACKUP_PATH" ]; then @@ -2485,22 +2955,6 @@ _initpath() { } -_exec() { - if [ -z "$_EXEC_TEMP_ERR" ]; then - _EXEC_TEMP_ERR="$(_mktemp)" - fi - - if [ "$_EXEC_TEMP_ERR" ]; then - eval "$@ 2>>$_EXEC_TEMP_ERR" - else - eval "$@" - fi -} - -_exec_err() { - [ "$_EXEC_TEMP_ERR" ] && _err "$(cat "$_EXEC_TEMP_ERR")" && echo "" >"$_EXEC_TEMP_ERR" -} - _apachePath() { _APACHECTL="apachectl" if ! _exists apachectl; then @@ -2513,8 +2967,7 @@ _apachePath() { fi fi - if ! _exec $_APACHECTL -V >/dev/null; then - _exec_err + if ! $_APACHECTL -V >/dev/null; then return 1 fi @@ -2566,8 +3019,7 @@ _restoreApache() { cat "$APACHE_CONF_BACKUP_DIR/$httpdconfname" >"$httpdconf" _debug "Restored: $httpdconf." - if ! _exec $_APACHECTL -t; then - _exec_err + if ! $_APACHECTL -t; then _err "Sorry, restore apache config error, please contact me." return 1 fi @@ -2585,8 +3037,7 @@ _setApache() { #test the conf first _info "Checking if there is an error in the apache config file before starting." - if ! _exec "$_APACHECTL" -t >/dev/null; then - _exec_err + if ! $_APACHECTL -t >/dev/null; then _err "The apache config file has error, please fix it first, then try again." _err "Don't worry, there is nothing changed to your system." return 1 @@ -2609,10 +3060,10 @@ _setApache() { apacheVer="$($_APACHECTL -V | grep "Server version:" | cut -d : -f 2 | cut -d " " -f 2 | cut -d '/' -f 2)" _debug "apacheVer" "$apacheVer" - apacheMajer="$(echo "$apacheVer" | cut -d . -f 1)" + apacheMajor="$(echo "$apacheVer" | cut -d . -f 1)" apacheMinor="$(echo "$apacheVer" | cut -d . -f 2)" - if [ "$apacheVer" ] && [ "$apacheMajer$apacheMinor" -ge "24" ]; then + if [ "$apacheVer" ] && [ "$apacheMajor$apacheMinor" -ge "24" ]; then echo " Alias /.well-known/acme-challenge $ACME_DIR @@ -2637,7 +3088,7 @@ Allow from all if _restoreApache; then _err "The apache config file is restored." else - _err "Sorry, The apache config file can not be restored, please report bug." + _err "Sorry, the apache config file can not be restored, please report bug." fi return 1 fi @@ -2647,8 +3098,7 @@ Allow from all chmod 755 "$ACME_DIR" fi - if ! _exec "$_APACHECTL" graceful; then - _exec_err + if ! $_APACHECTL graceful; then _err "$_APACHECTL graceful error, please contact me." _restoreApache return 1 @@ -2679,10 +3129,15 @@ _setNginx() { _err "nginx command is not found." return 1 fi - NGINX_CONF="$(nginx -V 2>&1 | _egrep_o "--conf-path=[^ ]* " | tr -d " ")" + NGINX_CONF="$(nginx -V 2>&1 | _egrep_o "\-\-conf-path=[^ ]* " | tr -d " ")" _debug NGINX_CONF "$NGINX_CONF" NGINX_CONF="$(echo "$NGINX_CONF" | cut -d = -f 2)" _debug NGINX_CONF "$NGINX_CONF" + if [ -z "$NGINX_CONF" ]; then + _err "Can not find nginx conf." + NGINX_CONF="" + return 1 + fi if [ ! -f "$NGINX_CONF" ]; then _err "'$NGINX_CONF' doesn't exist." NGINX_CONF="" @@ -2728,8 +3183,7 @@ _setNginx() { return 1 fi _info "Check the nginx conf before setting up." - if ! _exec "nginx -t" >/dev/null; then - _exec_err + if ! nginx -t >/dev/null; then return 1 fi @@ -2756,16 +3210,14 @@ location ~ \"^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)\$\" { fi _debug3 "Modified config:$(cat $FOUND_REAL_NGINX_CONF)" _info "nginx conf is done, let's check it again." - if ! _exec "nginx -t" >/dev/null; then - _exec_err + if ! nginx -t >/dev/null; then _err "It seems that nginx conf was broken, let's restore." cat "$_backup_conf" >"$FOUND_REAL_NGINX_CONF" return 1 fi _info "Reload nginx" - if ! _exec "nginx -s reload" >/dev/null; then - _exec_err + if ! nginx -s reload >/dev/null; then _err "It seems that nginx reload error, let's restore." cat "$_backup_conf" >"$FOUND_REAL_NGINX_CONF" return 1 @@ -2799,6 +3251,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 @@ -2843,7 +3300,7 @@ _isRealNginxConf() { _skip_ssl=1 for _listen_i in $(echo "$_seg_n" | tr "\t" ' ' | grep "^ *listen" | tr -d " "); do if [ "$_listen_i" ]; then - if [ "$(echo "$_listen_i" | _egrep_o "listen.*ssl[ |;]")" ]; then + if [ "$(echo "$_listen_i" | _egrep_o "listen.*ssl")" ]; then _debug2 "$_listen_i is ssl" else _debug2 "$_listen_i is plain text" @@ -2885,8 +3342,7 @@ _restoreNginx() { done _info "Reload nginx" - if ! _exec "nginx -s reload" >/dev/null; then - _exec_err + if ! nginx -s reload >/dev/null; then _err "It seems that nginx reload error, please report bug." return 1 fi @@ -2909,38 +3365,40 @@ _clearup() { _clearupdns() { _debug "_clearupdns" - if [ "$dnsadded" != 1 ] || [ -z "$vlist" ]; then + _debug "dns_entries" "$dns_entries" + + if [ -z "$dns_entries" ]; then _debug "skip dns." return fi _info "Removing DNS records." - ventries=$(echo "$vlist" | tr ',' ' ') - _alias_index=1 - for ventry in $ventries; do - d=$(echo "$ventry" | cut -d "$sep" -f 1) - keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) - vtype=$(echo "$ventry" | cut -d "$sep" -f 4) - _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) - txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _url_replace)" - _debug txt "$txt" - if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then - _debug "$d is already verified, skip $vtype." - continue - fi - if [ "$vtype" != "$VTYPE_DNS" ]; then - _debug "Skip $d for $vtype" - continue + for entry in $dns_entries; do + d=$(_getfield "$entry" 1) + txtdomain=$(_getfield "$entry" 2) + aliasDomain=$(_getfield "$entry" 3) + _currentRoot=$(_getfield "$entry" 4) + txt=$(_getfield "$entry" 5) + d_api=$(_getfield "$entry" 6) + _debug "d" "$d" + _debug "txtdomain" "$txtdomain" + _debug "aliasDomain" "$aliasDomain" + _debug "_currentRoot" "$_currentRoot" + _debug "txt" "$txt" + _debug "d_api" "$d_api" + if [ "$d_api" = "$txt" ]; then + d_api="" fi - d_api="$(_findHook "$d" dnsapi "$_currentRoot")" - _debug d_api "$d_api" - if [ -z "$d_api" ]; then _info "Not Found domain api file: $d_api" continue fi + if [ "$aliasDomain" ]; then + txtdomain="$aliasDomain" + fi + ( if ! . "$d_api"; then _err "Load file $d_api error. Please check your api file and try again." @@ -2952,29 +3410,12 @@ _clearupdns() { _err "It seems that your api file doesn't define $rmcommand" return 1 fi - - _dns_root_d="$d" - if _startswith "$_dns_root_d" "*."; then - _dns_root_d="$(echo "$_dns_root_d" | sed 's/*.//')" - fi - - _d_alias="$(_getfield "$_challenge_alias" "$_alias_index")" - _alias_index="$(_math "$_alias_index" + 1)" - _debug "_d_alias" "$_d_alias" - if [ "$_d_alias" ]; then - if _startswith "$_d_alias" "$DNS_ALIAS_PREFIX"; then - txtdomain="$(echo "$_d_alias" | sed "s/$DNS_ALIAS_PREFIX//")" - else - txtdomain="_acme-challenge.$_d_alias" - fi - else - txtdomain="_acme-challenge.$_dns_root_d" - fi - + _info "Removing txt: $txt for domain: $txtdomain" if ! $rmcommand "$txtdomain" "$txt"; then _err "Error removing txt for domain:$txtdomain" return 1 fi + _info "Removed: Success" ) done @@ -3024,6 +3465,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." @@ -3060,12 +3503,13 @@ _on_before_issue() { _info "Standalone mode." if [ -z "$Le_HTTPPort" ]; then Le_HTTPPort=80 + _cleardomainconf "Le_HTTPPort" else _savedomainconf "Le_HTTPPort" "$Le_HTTPPort" fi _checkport="$Le_HTTPPort" - elif [ "$_currentRoot" = "$W_TLS" ]; then - _info "Standalone tls mode." + elif [ "$_currentRoot" = "$W_ALPN" ]; then + _info "Standalone alpn mode." if [ -z "$Le_TLSPort" ]; then Le_TLSPort=443 else @@ -3084,7 +3528,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" @@ -3146,7 +3590,7 @@ _on_issue_err() { ) fi - if [ "$IS_RENEW" = "1" ] && _hasfield "$Le_Webroot" "$W_DNS"; then + if [ "$_ACME_IS_RENEW" = "1" ] && _hasfield "$Le_Webroot" "$W_DNS"; then _err "$_DNS_MANUAL_ERR" fi @@ -3160,10 +3604,16 @@ _on_issue_success() { _chk_post_hook="$1" _chk_renew_hook="$2" _debug _on_issue_success + #run the post hook if [ "$_chk_post_hook" ]; then _info "Run post hook:'$_chk_post_hook'" if ! ( + export CERT_PATH + export CERT_KEY_PATH + export CA_CERT_PATH + export CERT_FULLCHAIN_PATH + export Le_Domain="$_main_domain" cd "$DOMAIN_PATH" && eval "$_chk_post_hook" ); then _err "Error when run post hook." @@ -3172,9 +3622,14 @@ _on_issue_success() { fi #run renew hook - if [ "$IS_RENEW" ] && [ "$_chk_renew_hook" ]; then + if [ "$_ACME_IS_RENEW" ] && [ "$_chk_renew_hook" ]; then _info "Run renew hook:'$_chk_renew_hook'" if ! ( + export CERT_PATH + export CERT_KEY_PATH + export CA_CERT_PATH + export CERT_FULLCHAIN_PATH + export Le_Domain="$_main_domain" cd "$DOMAIN_PATH" && eval "$_chk_renew_hook" ); then _err "Error when run renew hook." @@ -3182,21 +3637,19 @@ _on_issue_success() { fi fi - if _hasfield "$Le_Webroot" "$W_DNS"; then + if _hasfield "$Le_Webroot" "$W_DNS" && [ -z "$FORCE_DNS_MANUAL" ]; then _err "$_DNS_MANUAL_WARN" fi } -updateaccount() { - _initpath - _regAccount -} - +#account_key_length eab-kid eab-hmac-key registeraccount() { - _reg_length="$1" + _account_key_length="$1" + _eab_id="$2" + _eab_hmac_key="$3" _initpath - _regAccount "$_reg_length" + _regAccount "$_account_key_length" "$_eab_id" "$_eab_hmac_key" } __calcAccountKeyHash() { @@ -3207,23 +3660,31 @@ __calc_account_thumbprint() { printf "%s" "$jwk" | tr -d ' ' | _digest "sha256" | _url_replace } +_getAccountEmail() { + if [ "$ACCOUNT_EMAIL" ]; then + echo "$ACCOUNT_EMAIL" + return 0 + fi + if [ -z "$CA_EMAIL" ]; then + CA_EMAIL="$(_readcaconf CA_EMAIL)" + fi + if [ "$CA_EMAIL" ]; then + echo "$CA_EMAIL" + return 0 + fi + _readaccountconf "ACCOUNT_EMAIL" +} + #keylength _regAccount() { _initpath _reg_length="$1" + _eab_id="$2" + _eab_hmac_key="$3" _debug3 _regAccount "$_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 @@ -3235,46 +3696,113 @@ _regAccount() { if ! _calcjwk "$ACCOUNT_KEY_PATH"; then return 1 fi - - if [ "$ACME_VERSION" = "2" ]; then - regjson='{"termsOfServiceAgreed": true}' - if [ "$ACCOUNT_EMAIL" ]; then - regjson='{"contact": ["mailto: '$ACCOUNT_EMAIL'"], "termsOfServiceAgreed": true}' - fi - else - _reg_res="$ACME_NEW_ACCOUNT_RES" - regjson='{"resource": "'$_reg_res'", "terms-of-service-agreed": true, "agreement": "'$ACME_AGREEMENT'"}' - if [ "$ACCOUNT_EMAIL" ]; then - regjson='{"resource": "'$_reg_res'", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "terms-of-service-agreed": true, "agreement": "'$ACME_AGREEMENT'"}' - fi + if [ "$_eab_id" ] && [ "$_eab_hmac_key" ]; then + _savecaconf CA_EAB_KEY_ID "$_eab_id" + _savecaconf CA_EAB_HMAC_KEY "$_eab_hmac_key" + fi + _eab_id=$(_readcaconf "CA_EAB_KEY_ID") + _eab_hmac_key=$(_readcaconf "CA_EAB_HMAC_KEY") + _secure_debug3 _eab_id "$_eab_id" + _secure_debug3 _eab_hmac_key "$_eab_hmac_key" + _email="$(_getAccountEmail)" + if [ "$_email" ]; then + _savecaconf "CA_EMAIL" "$_email" fi - _info "Registering account" + 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 + _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" if ! _send_signed_request "${ACME_NEW_ACCOUNT}" "$regjson"; then _err "Register account Error: $response" return 1 fi + _eabAlreadyBound="" if [ "$code" = "" ] || [ "$code" = '201' ]; then echo "$response" >"$ACCOUNT_JSON_PATH" _info "Registered" elif [ "$code" = '409' ] || [ "$code" = '200' ]; then _info "Already registered" + elif [ "$code" = '400' ] && _contains "$response" 'The account is not awaiting external account binding'; then + _info "Already register EAB." + _eabAlreadyBound=1 else _err "Register account Error: $response" return 1 fi - _debug2 responseHeaders "$responseHeaders" - _accUri="$(echo "$responseHeaders" | grep "^Location:" | _head_n 1 | cut -d ' ' -f 2 | tr -d "\r\n")" - _debug "_accUri" "$_accUri" - if [ -z "$_accUri" ]; then - _err "Can not find account id url." - _err "$responseHeaders" - return 1 + if [ -z "$_eabAlreadyBound" ]; then + _debug2 responseHeaders "$responseHeaders" + _accUri="$(echo "$responseHeaders" | grep -i "^Location:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n ")" + _debug "_accUri" "$_accUri" + if [ -z "$_accUri" ]; then + _err "Can not find account id url." + _err "$responseHeaders" + return 1 + fi + _savecaconf "ACCOUNT_URL" "$_accUri" + else + ACCOUNT_URL="$(_readcaconf ACCOUNT_URL)" fi - _savecaconf "ACCOUNT_URL" "$_accUri" export ACCOUNT_URL="$_accUri" CA_KEY_HASH="$(__calcAccountKeyHash)" @@ -3290,20 +3818,10 @@ _regAccount() { _info "ACCOUNT_THUMBPRINT" "$ACCOUNT_THUMBPRINT" } -#Implement deactivate account -deactivateaccount() { +#implement updateaccount +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 @@ -3323,11 +3841,50 @@ deactivateaccount() { fi _initAPI - if [ "$ACME_VERSION" = "2" ]; then - _djson="{\"status\":\"deactivated\"}" + _email="$(_getAccountEmail)" + + if [ "$_email" ]; then + updjson='{"contact": ["mailto:'$_email'"]}' else - _djson="{\"resource\": \"reg\", \"status\":\"deactivated\"}" + updjson='{"contact": []}' fi + + _send_signed_request "$_accUri" "$updjson" + + if [ "$code" = '200' ]; then + echo "$response" >"$ACCOUNT_JSON_PATH" + _info "Account update success for $_accUri." + else + _info "Error. The account was not updated." + return 1 + fi +} + +#Implement deactivate account +deactivateaccount() { + _initpath + + if [ ! -f "$ACCOUNT_KEY_PATH" ]; then + _err "Account key is not found at: $ACCOUNT_KEY_PATH" + return 1 + fi + + _accUri=$(_readcaconf "ACCOUNT_URL") + _debug _accUri "$_accUri" + + if [ -z "$_accUri" ]; then + _err "The account url is empty, please run '--update-account' first to update the account info first," + _err "Then try again." + return 1 + fi + + if ! _calcjwk "$ACCOUNT_KEY_PATH"; then + return 1 + fi + _initAPI + + _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 ' ,') @@ -3367,9 +3924,9 @@ _findHook() { d_api="$_SCRIPT_HOME/$_hookcat/$_hookname" elif [ -f "$_SCRIPT_HOME/$_hookcat/$_hookname.sh" ]; then d_api="$_SCRIPT_HOME/$_hookcat/$_hookname.sh" - elif [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname" ]; then + elif [ "$_hookdomain" ] && [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname" ]; then d_api="$LE_WORKING_DIR/$_hookdomain/$_hookname" - elif [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname.sh" ]; then + elif [ "$_hookdomain" ] && [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname.sh" ]; then d_api="$LE_WORKING_DIR/$_hookdomain/$_hookname.sh" elif [ -f "$LE_WORKING_DIR/$_hookname" ]; then d_api="$LE_WORKING_DIR/$_hookname" @@ -3425,22 +3982,358 @@ __get_domain_new_authz() { #uri keyAuthorization __trigger_validation() { - _debug2 "tigger domain validation." + _debug2 "Trigger domain validation." _t_url="$1" _debug2 _t_url "$_t_url" _t_key_authz="$2" _debug2 _t_key_authz "$_t_key_authz" - if [ "$ACME_VERSION" = "2" ]; then - _send_signed_request "$_t_url" "{\"keyAuthorization\": \"$_t_key_authz\"}" + _t_vtype="$3" + _debug2 _t_vtype "$_t_vtype" + + _send_signed_request "$_t_url" "{}" + +} + +#endpoint domain type +_ns_lookup_impl() { + _ns_ep="$1" + _ns_domain="$2" + _ns_type="$3" + _debug2 "_ns_ep" "$_ns_ep" + _debug2 "_ns_domain" "$_ns_domain" + _debug2 "_ns_type" "$_ns_type" + + response="$(_H1="accept: application/dns-json" _get "$_ns_ep?name=$_ns_domain&type=$_ns_type")" + _ret=$? + _debug2 "response" "$response" + if [ "$_ret" != "0" ]; then + return $_ret + fi + _answers="$(echo "$response" | tr '{}' '<>' | _egrep_o '"Answer":\[[^]]*]' | tr '<>' '\n\n')" + _debug2 "_answers" "$_answers" + echo "$_answers" +} + +#domain, type +_ns_lookup_cf() { + _cf_ld="$1" + _cf_ld_type="$2" + _cf_ep="https://cloudflare-dns.com/dns-query" + _ns_lookup_impl "$_cf_ep" "$_cf_ld" "$_cf_ld_type" +} + +#domain, type +_ns_purge_cf() { + _cf_d="$1" + _cf_d_type="$2" + _debug "Cloudflare purge $_cf_d_type record for domain $_cf_d" + _cf_purl="https://cloudflare-dns.com/api/v1/purge?domain=$_cf_d&type=$_cf_d_type" + response="$(_post "" "$_cf_purl")" + _debug2 response "$response" +} + +#checks if cf server is available +_ns_is_available_cf() { + if _get "https://cloudflare-dns.com" "" 10 >/dev/null; then + return 0 else - _send_signed_request "$_t_url" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$_t_key_authz\"}" + return 1 + fi +} + +_ns_is_available_google() { + if _get "https://dns.google" "" 10 >/dev/null; then + return 0 + else + return 1 + fi +} + +#domain, type +_ns_lookup_google() { + _cf_ld="$1" + _cf_ld_type="$2" + _cf_ep="https://dns.google/resolve" + _ns_lookup_impl "$_cf_ep" "$_cf_ld" "$_cf_ld_type" +} + +_ns_is_available_ali() { + if _get "https://dns.alidns.com" "" 10 >/dev/null; 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" "" 10 >/dev/null; 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" +} + +_ns_select_doh() { + if [ -z "$DOH_USE" ]; then + _debug "Detect dns server first." + if _ns_is_available_cf; then + _debug "Use cloudflare doh server" + export DOH_USE=$DOH_CLOUDFLARE + 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 +} + +#domain, type +_ns_lookup() { + _ns_select_doh + if [ "$DOH_USE" = "$DOH_CLOUDFLARE" ] || [ -z "$DOH_USE" ]; then + _ns_lookup_cf "$@" + 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 + +} + +#txtdomain, alias, txt +__check_txt() { + _c_txtdomain="$1" + _c_aliasdomain="$2" + _c_txt="$3" + _debug "_c_txtdomain" "$_c_txtdomain" + _debug "_c_aliasdomain" "$_c_aliasdomain" + _debug "_c_txt" "$_c_txt" + _ns_select_doh + _answers="$(_ns_lookup "$_c_aliasdomain" TXT)" + _contains "$_answers" "$_c_txt" + +} + +#txtdomain +__purge_txt() { + _p_txtdomain="$1" + _debug _p_txtdomain "$_p_txtdomain" + if [ "$DOH_USE" = "$DOH_CLOUDFLARE" ] || [ -z "$DOH_USE" ]; then + _ns_purge_cf "$_p_txtdomain" "TXT" + else + _debug "no purge api for this doh api, just sleep 5 secs" + _sleep 5 + fi + +} + +#wait and check each dns entries +_check_dns_entries() { + _success_txt="," + _end_time="$(_time)" + _end_time="$(_math "$_end_time" + 1200)" #let's check no more than 20 minutes. + + while [ "$(_time)" -le "$_end_time" ]; do + _info "You can use '--dnssleep' to disable public dns checks." + _info "See: $_DNSCHECK_WIKI" + _left="" + for entry in $dns_entries; do + d=$(_getfield "$entry" 1) + txtdomain=$(_getfield "$entry" 2) + txtdomain=$(_idn "$txtdomain") + aliasDomain=$(_getfield "$entry" 3) + aliasDomain=$(_idn "$aliasDomain") + txt=$(_getfield "$entry" 5) + d_api=$(_getfield "$entry" 6) + _debug "d" "$d" + _debug "txtdomain" "$txtdomain" + _debug "aliasDomain" "$aliasDomain" + _debug "txt" "$txt" + _debug "d_api" "$d_api" + _info "Checking $d for $aliasDomain" + if _contains "$_success_txt" ",$txt,"; then + _info "Already success, continue next one." + continue + fi + + if __check_txt "$txtdomain" "$aliasDomain" "$txt"; then + _info "Domain $d '$aliasDomain' success." + _success_txt="$_success_txt,$txt," + continue + fi + _left=1 + _info "Not valid yet, let's wait 10 seconds and check next one." + __purge_txt "$txtdomain" + if [ "$txtdomain" != "$aliasDomain" ]; then + __purge_txt "$aliasDomain" + fi + _sleep 10 + done + if [ "$_left" ]; then + _info "Let's wait 10 seconds and check again". + _sleep 10 + else + _info "All success, let's return" + return 0 + fi + done + _info "Timed out waiting for DNS." + return 1 + +} + +#file +_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)" "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 + _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 +} + +#cert issuer +_match_issuer() { + _cfile="$1" + _missuer="$2" + _fissuers="$(_get_chain_issuers $_cfile)" + _debug2 _fissuers "$_fissuers" + _rootissuer="$(echo "$_fissuers" | _lower_case | _tail_n 1)" + _debug2 _rootissuer "$_rootissuer" + _missuer="$(echo "$_missuer" | _lower_case)" + _contains "$_rootissuer" "$_missuer" +} + +#ip +_isIPv4() { + for seg in $(echo "$1" | tr '.' ' '); do + _debug2 seg "$seg" + if [ "$(echo "$seg" | tr -d '[0-9]')" ]; then + #not all number + return 1 + fi + if [ $seg -ge 0 ] && [ $seg -lt 256 ]; then + continue + fi + return 1 + done + return 0 +} + +#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 +} + +# beginTime dateTo +# beginTime is full string format("2022-04-01T08:10:33Z"), beginTime can be empty, to use current time +# dateTo can be ether in full string format("2022-04-01T08:10:33Z") or in delta format(+5d or +20h) +_convertValidaty() { + _beginTime="$1" + _dateTo="$2" + _debug2 "_beginTime" "$_beginTime" + _debug2 "_dateTo" "$_dateTo" + + if _startswith "$_dateTo" "+"; then + _v_begin=$(_time) + if [ "$_beginTime" ]; then + _v_begin="$(_date2time "$_beginTime")" + fi + _debug2 "_v_begin" "$_v_begin" + if _endswith "$_dateTo" "h"; then + _v_end=$(_math "$_v_begin + 60 * 60 * $(echo "$_dateTo" | tr -d '+h')") + elif _endswith "$_dateTo" "d"; then + _v_end=$(_math "$_v_begin + 60 * 60 * 24 * $(echo "$_dateTo" | tr -d '+d')") + else + _err "Not recognized format for _dateTo: $_dateTo" + return 1 + fi + _debug2 "_v_end" "$_v_end" + _time2str "$_v_end" + else + if [ "$(_time)" -gt "$(_date2time "$_dateTo")" ]; then + _err "The validaty to is in the past: _dateTo = $_dateTo" + return 1 + fi + echo "$_dateTo" fi } #webroot, domain domainlist keylength issue() { if [ -z "$2" ]; then - _usage "Usage: $PROJECT_ENTRY --issue -d a.com -w /path/to/webroot/a.com/ " + _usage "Usage: $PROJECT_ENTRY --issue --domain --webroot " return 1 fi if [ -z "$1" ]; then @@ -3469,20 +4362,17 @@ issue() { _renew_hook="${12}" _local_addr="${13}" _challenge_alias="${14}" - #remove these later. - if [ "$_web_roots" = "dns-cf" ]; then - _web_roots="dns_cf" - fi - if [ "$_web_roots" = "dns-dp" ]; then - _web_roots="dns_dp" - fi - if [ "$_web_roots" = "dns-cx" ]; then - _web_roots="dns_cx" - fi + _preferred_chain="${15}" + _valid_from="${16}" + _valid_to="${17}" - if [ ! "$IS_RENEW" ]; then + 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 @@ -3490,19 +4380,28 @@ 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" if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + _valid_to_saved=$(_readdomainconf Le_Valid_to) + if [ "$_valid_to_saved" ] && ! _startswith "$_valid_to_saved" "+"; then + _info "The domain is set to be valid to: $_valid_to_saved" + _info "It can not be renewed automatically" + _info "See: $_VALIDITY_WIKI" + return $RENEW_SKIP + fi _saved_domain=$(_readdomainconf Le_Domain) _debug _saved_domain "$_saved_domain" _saved_alt=$(_readdomainconf Le_Alt) _debug _saved_alt "$_saved_alt" - if [ "$_saved_domain,$_saved_alt" = "$_main_domain,$_alt_domains" ]; then + _normized_saved_domains="$(echo "$_saved_domain,$_saved_alt" | tr "," "\n" | sort | tr '\n' ',')" + _debug _normized_saved_domains "$_normized_saved_domains" + + _normized_domains="$(echo "$_main_domain,$_alt_domains" | tr "," "\n" | sort | tr '\n' ',')" + _debug _normized_domains "$_normized_domains" + + if [ "$_normized_saved_domains" = "$_normized_domains" ]; then _info "Domains not changed." _info "Skip, Next renewal time is: $(__green "$(_readdomainconf Le_NextRenewTimeStr)")" _info "Add '$(__red '--force')' to force to renew." @@ -3513,13 +4412,18 @@ 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" - _savedomainconf "Le_PreHook" "$_pre_hook" - _savedomainconf "Le_PostHook" "$_post_hook" - _savedomainconf "Le_RenewHook" "$_renew_hook" + _savedomainconf "Le_PreHook" "$_pre_hook" "base64" + _savedomainconf "Le_PostHook" "$_post_hook" "base64" + _savedomainconf "Le_RenewHook" "$_renew_hook" "base64" if [ "$_local_addr" ]; then _savedomainconf "Le_LocalAddress" "$_local_addr" @@ -3531,18 +4435,20 @@ issue() { else _cleardomainconf "Le_ChallengeAlias" fi + if [ "$_preferred_chain" ]; then + _savedomainconf "Le_Preferred_Chain" "$_preferred_chain" "base64" + else + _cleardomainconf "Le_Preferred_Chain" + fi Le_API="$ACME_DIRECTORY" _savedomainconf "Le_API" "$Le_API" + _info "Using CA: $ACME_DIRECTORY" if [ "$_alt_domains" = "$NO_VALUE" ]; then _alt_domains="" fi - if [ "$_key_length" = "$NO_VALUE" ]; then - _key_length="" - fi - if ! _on_before_issue "$_web_roots" "$_main_domain" "$_alt_domains" "$_pre_hook" "$_local_addr"; then _err "_on_before_issue." return 1 @@ -3560,20 +4466,43 @@ issue() { _debug "_saved_account_key_hash is not changed, skip register account." fi + export Le_Next_Domain_Key="$CERT_KEY_PATH.next" if [ -f "$CSR_PATH" ] && [ ! -f "$CERT_KEY_PATH" ]; then _info "Signing from existing CSR." else + # When renewing from an old version, the empty Le_Keylength means 2048. + # Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over + # time but an empty value implies 2048 specifically. _key=$(_readdomainconf Le_Keylength) + if [ -z "$_key" ]; then + _key=2048 + fi _debug "Read key length:$_key" if [ ! -f "$CERT_KEY_PATH" ] || [ "$_key_length" != "$_key" ] || [ "$Le_ForceNewDomainKey" = "1" ]; then - if ! createDomainKey "$_main_domain" "$_key_length"; then - _err "Create domain key error." - _clearup - _on_issue_err "$_post_hook" + if [ "$Le_ForceNewDomainKey" = "1" ] && [ -f "$Le_Next_Domain_Key" ]; then + _info "Using pre generated key: $Le_Next_Domain_Key" + cat "$Le_Next_Domain_Key" >"$CERT_KEY_PATH" + echo "" >"$Le_Next_Domain_Key" + else + if ! createDomainKey "$_main_domain" "$_key_length"; then + _err "Create domain key error." + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + fi + fi + if [ "$Le_ForceNewDomainKey" ]; then + _info "Generate next pre-generate key." + if [ ! -e "$Le_Next_Domain_Key" ]; then + touch "$Le_Next_Domain_Key" + chmod 600 "$Le_Next_Domain_Key" + fi + if ! _createkey "$_key_length" "$Le_Next_Domain_Key"; then + _err "Can not pre generate domain key" return 1 fi fi - if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then _err "Create CSR error." _clearup @@ -3585,78 +4514,118 @@ issue() { _savedomainconf "Le_Keylength" "$_key_length" vlist="$Le_Vlist" - + _cleardomainconf "Le_Vlist" _info "Getting domain auth token for each domain" sep='#' dvsep=',' if [ -z "$vlist" ]; then - if [ "$ACME_VERSION" = "2" ]; then - #make new order request - _identifiers="{\"type\":\"dns\",\"value\":\"$_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\":\"$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" + #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 + _identifiers="$_identifiers,{\"type\":\"$(_getIdType "$d")\",\"value\":\"$(_idn "$d")\"}" + done + _debug2 _identifiers "$_identifiers" + _notBefore="" + _notAfter="" + + if [ "$_valid_from" ]; then + _savedomainconf "Le_Valid_From" "$_valid_from" + _debug2 "_valid_from" "$_valid_from" + _notBefore="$(_convertValidaty "" "$_valid_from")" + if [ "$?" != "0" ]; then + _err "Can not parse _valid_from: $_valid_from" return 1 fi - - Le_OrderFinalize="$(echo "$response" | tr -d '\r\n' | _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 + if [ "$(_time)" -gt "$(_date2time "$_notBefore")" ]; then + _notBefore="" fi - - #for dns manual mode - _savedomainconf "Le_OrderFinalize" "$Le_OrderFinalize" - - _authorizations_seg="$(echo "$response" | tr -d '\r\n' | _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 ! response="$(_get "$_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" + else + _cleardomainconf "Le_Valid_From" fi + _debug2 _notBefore "$_notBefore" + + if [ "$_valid_to" ]; then + _debug2 "_valid_to" "$_valid_to" + _savedomainconf "Le_Valid_To" "$_valid_to" + _notAfter="$(_convertValidaty "$_notBefore" "$_valid_to")" + if [ "$?" != "0" ]; then + _err "Can not parse _valid_to: $_valid_to" + return 1 + fi + else + _cleardomainconf "Le_Valid_To" + fi + _debug2 "_notAfter" "$_notAfter" + + _newOrderObj="{\"identifiers\": [$_identifiers]" + if [ "$_notBefore" ]; then + _newOrderObj="$_newOrderObj,\"notBefore\": \"$_notBefore\"" + fi + if [ "$_notAfter" ]; then + _newOrderObj="$_newOrderObj,\"notAfter\": \"$_notAfter\"" + fi + if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; 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#$_authz_url +$_authorizations_map" + done + + _debug2 _authorizations_map "$_authorizations_map" _index=0 _currentRoot="" @@ -3683,40 +4652,49 @@ $_authorizations_map" vtype="$VTYPE_DNS" fi - if [ "$_currentRoot" = "$W_TLS" ]; then - if [ "$ACME_VERSION" = "2" ]; then - vtype="$VTYPE_TLS2" - else - vtype="$VTYPE_TLS" - fi + if [ "$_currentRoot" = "$W_ALPN" ]; then + vtype="$VTYPE_ALPN" fi - if [ "$ACME_VERSION" = "2" ]; then - response="$(echo "$_authorizations_map" | grep "^$d," | sed "s/$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 + _authz_url="$(echo "$_candidates" | sed "s/$_idn_d,//" | _egrep_o "#.*" | sed "s/^#//")" + _debug _authz_url "$_authz_url" if [ -z "$thumbprint" ]; then thumbprint="$(__calc_account_thumbprint)" fi - entry="$(printf "%s\n" "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" + keyauthorization="" + + if echo "$response" | grep '"status":"valid"' >/dev/null 2>&1; then + _debug "$d is already valid." + keyauthorization="$STATE_VERIFIED" + _debug keyauthorization "$keyauthorization" + fi + + entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" - if [ -z "$entry" ]; then - _err "Error, can not get domain token entry $d" + + if [ -z "$keyauthorization" -a -z "$entry" ]; then + _err "Error, can not get domain token entry $d for $vtype" _supported_vtypes="$(echo "$response" | _egrep_o "\"challenges\":\[[^]]*]" | tr '{' "\n" | grep type | cut -d '"' -f 4 | tr "\n" ' ')" if [ "$_supported_vtypes" ]; then _err "The supported validation types are: $_supported_vtypes, but you specified: $vtype" @@ -3725,38 +4703,33 @@ $_authorizations_map" _on_issue_err "$_post_hook" return 1 fi - token="$(printf "%s\n" "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" - _debug token "$token" - if [ -z "$token" ]; then - _err "Error, can not get domain token $entry" - _clearup - _on_issue_err "$_post_hook" - return 1 - fi - if [ "$ACME_VERSION" = "2" ]; then - uri="$(printf "%s\n" "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)" - else - uri="$(printf "%s\n" "$entry" | _egrep_o '"uri":"[^"]*' | cut -d '"' -f 4)" - fi - _debug uri "$uri" + if [ -z "$keyauthorization" ]; then + token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" + _debug token "$token" - if [ -z "$uri" ]; then - _err "Error, can not get domain uri. $entry" - _clearup - _on_issue_err "$_post_hook" - return 1 - fi - keyauthorization="$token.$thumbprint" - _debug keyauthorization "$keyauthorization" + if [ -z "$token" ]; then + _err "Error, can not get domain token $entry" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi - if printf "%s" "$response" | grep '"status":"valid"' >/dev/null 2>&1; then - _debug "$d is already verified." - keyauthorization="$STATE_VERIFIED" + uri="$(echo "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)" + + _debug uri "$uri" + + if [ -z "$uri" ]; then + _err "Error, can not get domain uri. $entry" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + keyauthorization="$token.$thumbprint" _debug keyauthorization "$keyauthorization" fi - dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot" + dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot$sep$_authz_url" _debug dvlist "$dvlist" vlist="$vlist$dvlist$dvsep" @@ -3764,6 +4737,7 @@ $_authorizations_map" done _debug vlist "$vlist" #add entry + dns_entries="" dnsadded="" ventries=$(echo "$vlist" | tr "$dvsep" ' ') _alias_index=1 @@ -3772,9 +4746,11 @@ $_authorizations_map" keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) vtype=$(echo "$ventry" | cut -d "$sep" -f 4) _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) + _authz_url=$(echo "$ventry" | cut -d "$sep" -f 6) _debug d "$d" if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then _debug "$d is already verified, skip $vtype." + _alias_index="$(_math "$_alias_index" + 1)" continue fi @@ -3785,6 +4761,7 @@ $_authorizations_map" _dns_root_d="$(echo "$_dns_root_d" | sed 's/*.//')" fi _d_alias="$(_getfield "$_challenge_alias" "$_alias_index")" + test "$_d_alias" = "$NO_VALUE" && _d_alias="" _alias_index="$(_math "$_alias_index" + 1)" _debug "_d_alias" "$_d_alias" if [ "$_d_alias" ]; then @@ -3793,19 +4770,23 @@ $_authorizations_map" else txtdomain="_acme-challenge.$_d_alias" fi + dns_entry="${_dns_root_d}${dvsep}_acme-challenge.$_dns_root_d$dvsep$txtdomain$dvsep$_currentRoot" else txtdomain="_acme-challenge.$_dns_root_d" + dns_entry="${_dns_root_d}${dvsep}_acme-challenge.$_dns_root_d$dvsep$dvsep$_currentRoot" fi + _debug txtdomain "$txtdomain" txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _url_replace)" _debug txt "$txt" - d_api="$(_findHook "$_dns_root_d" dnsapi "$_currentRoot")" - + d_api="$(_findHook "$_dns_root_d" $_SUB_FOLDER_DNSAPI "$_currentRoot")" _debug d_api "$d_api" + dns_entry="$dns_entry$dvsep$txt${dvsep}$d_api" + _debug2 dns_entry "$dns_entry" if [ "$d_api" ]; then - _info "Found domain api file: $d_api" + _debug "Found domain api file: $d_api" else if [ "$_currentRoot" != "$W_DNS" ]; then _err "Can not find dns api hook for: $_currentRoot" @@ -3830,18 +4811,22 @@ $_authorizations_map" _err "It seems that your api file is not correct, it must have a function named: $addcommand" return 1 fi - + _info "Adding txt value: $txt for domain: $txtdomain" if ! $addcommand "$txtdomain" "$txt"; then _err "Error add txt for domain:$txtdomain" return 1 fi + _info "The txt record is added: Success." ) if [ "$?" != "0" ]; then - _clearup _on_issue_err "$_post_hook" "$vlist" + _clearup return 1 fi + dns_entries="$dns_entries$dns_entry +" + _debug2 "$dns_entries" dnsadded='1' fi done @@ -3850,22 +4835,30 @@ $_authorizations_map" _savedomainconf "Le_Vlist" "$vlist" _debug "Dns record not added yet, so, save to $DOMAIN_CONF and exit." _err "Please add the TXT records to the domains, and re-run with --renew." - _clearup _on_issue_err "$_post_hook" - return 1 + _clearup + # If asked to be in manual DNS mode, flag this exit with a separate + # error so it can be distinguished from other failures. + return $CODE_DNS_MANUAL fi fi - if [ "$dnsadded" = '1' ]; then + if [ "$dns_entries" ]; then if [ -z "$Le_DNSSleep" ]; then - Le_DNSSleep="$DEFAULT_DNS_SLEEP" + _info "Let's check each DNS record now. Sleep 20 seconds first." + _sleep 20 + if ! _check_dns_entries; then + _err "check dns error." + _on_issue_err "$_post_hook" + _clearup + return 1 + fi else _savedomainconf "Le_DNSSleep" "$Le_DNSSleep" + _info "Sleep $(__green $Le_DNSSleep) seconds for the txt records to take effect" + _sleep "$Le_DNSSleep" fi - - _info "Sleep $(__green $Le_DNSSleep) seconds for the txt records to take effect" - _sleep "$Le_DNSSleep" fi NGINX_RESTORE_VLIST="" @@ -3879,16 +4872,17 @@ $_authorizations_map" uri=$(echo "$ventry" | cut -d "$sep" -f 3) vtype=$(echo "$ventry" | cut -d "$sep" -f 4) _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) - + _authz_url=$(echo "$ventry" | cut -d "$sep" -f 6) if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then _info "$d is already verified, skip $vtype." continue fi - _info "Verifying:$d" + _info "Verifying: $d" _debug "d" "$d" _debug "keyauthorization" "$keyauthorization" _debug "uri" "$uri" + _debug "_authz_url" "$_authz_url" removelevel="" token="$(printf "%s" "$keyauthorization" | cut -d '.' -f 1)" @@ -3955,48 +4949,14 @@ $_authorizations_map" _on_issue_err "$_post_hook" "$vlist" return 1 fi - - if [ ! "$usingApache" ]; then - if webroot_owner=$(_stat "$_currentRoot"); then - _debug "Changing owner/group of .well-known to $webroot_owner" - if ! _exec "chown -R \"$webroot_owner\" \"$_currentRoot/.well-known\""; then - _debug "$(cat "$_EXEC_TEMP_ERR")" - _exec_err >/dev/null 2>&1 - fi - else - _debug "not changing owner/group of webroot" - fi + if ! chmod a+r "$wellknown_path/$token"; then + _debug "chmod failed, but we just continue." fi - fi - - elif [ "$vtype" = "$VTYPE_TLS" ]; then - #create A - #_hash_A="$(printf "%s" $token | _digest "sha256" "hex" )" - #_debug2 _hash_A "$_hash_A" - #_x="$(echo $_hash_A | cut -c 1-32)" - #_debug2 _x "$_x" - #_y="$(echo $_hash_A | cut -c 33-64)" - #_debug2 _y "$_y" - #_SAN_A="$_x.$_y.token.acme.invalid" - #_debug2 _SAN_A "$_SAN_A" - - #create B - _hash_B="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")" - _debug2 _hash_B "$_hash_B" - _x="$(echo "$_hash_B" | cut -c 1-32)" - _debug2 _x "$_x" - _y="$(echo "$_hash_B" | cut -c 33-64)" - _debug2 _y "$_y" - - #_SAN_B="$_x.$_y.ka.acme.invalid" - - _SAN_B="$_x.$_y.acme.invalid" - _debug2 _SAN_B "$_SAN_B" - - _ncaddr="$(_getfield "$_local_addr" "$_ncIndex")" - _ncIndex="$(_math "$_ncIndex" + 1)" - if ! _starttlsserver "$_SAN_B" "$_SAN_A" "$Le_TLSPort" "$keyauthorization" "$_ncaddr"; then + elif [ "$vtype" = "$VTYPE_ALPN" ]; then + acmevalidationv1="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")" + _debug acmevalidationv1 "$acmevalidationv1" + if ! _starttlsserver "$d" "" "$Le_TLSPort" "$keyauthorization" "$_ncaddr" "$acmevalidationv1"; then _err "Start tls server error." _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup @@ -4005,7 +4965,7 @@ $_authorizations_map" fi fi - if ! __trigger_validation "$uri" "$keyauthorization"; then + if ! __trigger_validation "$uri" "$keyauthorization" "$vtype"; then _err "$d:Can not get challenge: $response" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup @@ -4014,7 +4974,7 @@ $_authorizations_map" fi if [ "$code" ] && [ "$code" != '202' ]; then - if [ "$ACME_VERSION" = "2" ] && [ "$code" = '200' ]; then + if [ "$code" = '200' ]; then _debug "trigger validation code: $code" else _err "$d:Challenge error: $response" @@ -4030,6 +4990,7 @@ $_authorizations_map" MAX_RETRY_TIMES=30 fi + _debug "Lets check the status of the authz" while true; do waittimes=$(_math "$waittimes" + 1) if [ "$waittimes" -ge "$MAX_RETRY_TIMES" ]; then @@ -4040,40 +5001,22 @@ $_authorizations_map" return 1 fi - _debug "sleep 2 secs to verify" - sleep 2 - _debug "checking" - response="$(_get "$uri")" - 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 - error="$(echo "$response" | tr -d "\r\n" | _egrep_o '"error":\{[^\}]*')" + _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)" _debug2 errordetail "$errordetail" if [ "$errordetail" ]; then - _err "$d:Verify error:$errordetail" + _err "Invalid status, $d:Verify error detail:$errordetail" else - _err "$d:Verify error:$error" + _err "Invalid status, $d:Verify error:$error" fi if [ "$DEBUG" ]; then if [ "$vtype" = "$VTYPE_HTTP" ]; then @@ -4087,16 +5030,38 @@ $_authorizations_map" return 1 fi - if [ "$status" = "pending" ]; then - _info "Pending" + if _contains "$status" "valid"; then + _info "$(__green Success)" + _stopserver "$serverproc" + serverproc="" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + break + fi + + if _contains "$status" "pending"; then + _info "Pending, The CA is processing your order, please just wait. ($waittimes/$MAX_RETRY_TIMES)" + elif _contains "$status" "processing"; then + _info "Processing, The CA is processing your order, please just wait. ($waittimes/$MAX_RETRY_TIMES)" else - _err "$d:Verify error:$response" + _err "Unknown status: $status, $d:Verify error:$response" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup _on_issue_err "$_post_hook" "$vlist" return 1 fi + _debug "sleep 2 secs to verify again" + _sleep 2 + _debug "checking" + _send_signed_request "$_authz_url" + + if [ "$?" != "0" ]; then + _err "Invalid code, $d:Verify error:$response" + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err "$_post_hook" "$vlist" + return 1 + fi done done @@ -4105,57 +5070,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 - 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, code is not 200." + _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" + + _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 + else + _err "Sign error, wrong status" _err "$response" _on_issue_err "$_post_hook" return 1 fi - Le_LinkCert="$(echo "$response" | tr -d '\r\n' | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)" - - if ! _get "$Le_LinkCert" >"$CERT_PATH"; 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 - if [ "$(grep -- "$BEGIN_CERT" "$CERT_PATH" | wc -l)" -gt "1" ]; then - _debug "Found cert chain" - cat "$CERT_PATH" >"$CERT_FULLCHAIN_PATH" - _end_n="$(grep -n -- "$END_CERT" "$CERT_FULLCHAIN_PATH" | _head_n 1 | cut -d : -f 1)" - _debug _end_n "$_end_n" - sed -n "1,${_end_n}p" "$CERT_FULLCHAIN_PATH" >"$CERT_PATH" - _end_n="$(_math $_end_n + 1)" - sed -n "${_end_n},9999p" "$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 + + 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" @@ -4172,77 +5209,32 @@ $_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" ] || [ ! "$IN_CRON" ]; then + if [ ! "$USER_PATH" ] || [ ! "$_ACME_IN_CRON" ]; then USER_PATH="$PATH" _saveaccountconf "USER_PATH" "$USER_PATH" fi fi - _cleardomainconf "Le_Vlist" - - 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 _get "$Le_LinkIssuer" >"$CA_CERT_PATH"; then - 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 + [ -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")" + if [ "$Le_ForceNewDomainKey" ] && [ -e "$Le_Next_Domain_Key" ]; then + _info "Your pre-generated next key for future cert key change is in: $(__green "$Le_Next_Domain_Key")" 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 ")" Le_CertCreateTime=$(_time) _savedomainconf "Le_CertCreateTime" "$Le_CertCreateTime" - Le_CertCreateTimeStr=$(date -u) + Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime") _savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr" - if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ] || [ "$Le_RenewalDays" -gt "$MAX_RENEW" ]; then - Le_RenewalDays="$MAX_RENEW" + if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ]; then + Le_RenewalDays="$DEFAULT_RENEW" else _savedomainconf "Le_RenewalDays" "$Le_RenewalDays" fi @@ -4278,20 +5270,41 @@ $_authorizations_map" else _cleardomainconf Le_ForceNewDomainKey fi - - Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) - - Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + if [ "$_notAfter" ]; then + Le_NextRenewTime=$(_date2time "$_notAfter") + Le_NextRenewTimeStr="$_notAfter" + if [ "$_valid_to" ] && ! _startswith "$_valid_to" "+"; then + _info "The domain is set to be valid to: $_valid_to" + _info "It can not be renewed automatically" + _info "See: $_VALIDITY_WIKI" + else + _now=$(_time) + _debug2 "_now" "$_now" + _lifetime=$(_math $Le_NextRenewTime - $_now) + _debug2 "_lifetime" "$_lifetime" + if [ $_lifetime -gt 86400 ]; then + #if lifetime is logner than one day, it will renew one day before + Le_NextRenewTime=$(_math $Le_NextRenewTime - 86400) + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + else + #if lifetime is less than 24 hours, it will renew one hour before + Le_NextRenewTime=$(_math $Le_NextRenewTime - 3600) + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + fi + fi + else + Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) + Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + fi _savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr" - - Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime" if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then _savedomainconf "Le_RealCertPath" "$_real_cert" _savedomainconf "Le_RealCACertPath" "$_real_ca" _savedomainconf "Le_RealKeyPath" "$_real_key" - _savedomainconf "Le_ReloadCmd" "$_reload_cmd" + _savedomainconf "Le_ReloadCmd" "$_reload_cmd" "base64" _savedomainconf "Le_RealFullChainPath" "$_real_fullchain" if ! _installcert "$_main_domain" "$_real_cert" "$_real_key" "$_real_ca" "$_real_fullchain" "$_reload_cmd"; then return 1 @@ -4304,22 +5317,41 @@ $_authorizations_map" fi } -#domain [isEcc] +#in_out_cert out_fullchain out_ca +_split_cert_chain() { + _certf="$1" + _fullchainf="$2" + _caf="$3" + if [ "$(grep -- "$BEGIN_CERT" "$_certf" | wc -l)" -gt "1" ]; then + _debug "Found cert chain" + cat "$_certf" >"$_fullchainf" + _end_n="$(grep -n -- "$END_CERT" "$_fullchainf" | _head_n 1 | cut -d : -f 1)" + _debug _end_n "$_end_n" + sed -n "1,${_end_n}p" "$_fullchainf" >"$_certf" + _end_n="$(_math $_end_n + 1)" + sed -n "${_end_n},9999p" "$_fullchainf" >"$_caf" + fi +} + +#domain [isEcc] [server] renew() { Le_Domain="$1" if [ -z "$Le_Domain" ]; then - _usage "Usage: $PROJECT_ENTRY --renew -d domain.com [--ecc]" + _usage "Usage: $PROJECT_ENTRY --renew --domain [--ecc] [--server server]" return 1 fi _isEcc="$2" + _renewServer="$3" + _debug "_renewServer" "$_renewServer" _initpath "$Le_Domain" "$_isEcc" + _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _info "$(__green "Renew: '$Le_Domain'")" if [ ! -f "$DOMAIN_CONF" ]; then - _info "'$Le_Domain' is not a issued domain, skip." - return 0 + _info "'$Le_Domain' is not an issued domain, skip." + return $RENEW_SKIP fi if [ "$Le_RenewalDays" ]; then @@ -4328,37 +5360,65 @@ renew() { . "$DOMAIN_CONF" _debug Le_API "$Le_API" - if [ "$Le_API" ]; then - if [ "$_OLD_CA_HOST" = "$Le_API" ]; then - export Le_API="$DEFAULT_CA" - _savedomainconf Le_API "$Le_API" - fi - if [ "$_OLD_STAGE_CA_HOST" = "$Le_API" ]; then - export Le_API="$DEFAULT_STAGING_CA" - _savedomainconf Le_API "$Le_API" - fi - export ACME_DIRECTORY="$Le_API" - #reload ca configs - ACCOUNT_KEY_PATH="" - ACCOUNT_JSON_PATH="" - CA_CONF="" - _debug3 "initpath again." - _initpath "$Le_Domain" "$_isEcc" + + case "$Le_API" in + "$CA_LETSENCRYPT_V2_TEST") + _info "Switching back to $CA_LETSENCRYPT_V2" + Le_API="$CA_LETSENCRYPT_V2" + ;; + "$CA_BUYPASS_TEST") + _info "Switching back to $CA_BUYPASS" + Le_API="$CA_BUYPASS" + ;; + "$CA_GOOGLE_TEST") + _info "Switching back to $CA_GOOGLE" + Le_API="$CA_GOOGLE" + ;; + esac + + if [ "$_server" ]; then + Le_API="$_server" fi + _info "Renew to Le_API=$Le_API" + + _clearAPI + _clearCA + export ACME_DIRECTORY="$Le_API" + + #reload ca configs + _debug2 "initpath again." + _initpath "$Le_Domain" "$_isEcc" if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then _info "Skip, Next renewal time is: $(__green "$Le_NextRenewTimeStr")" _info "Add '$(__red '--force')' to force to renew." + if [ -z "$_ACME_IN_RENEWALL" ]; then + if [ $_set_level -ge $NOTIFY_LEVEL_SKIP ]; then + _send_notify "Renew $Le_Domain skipped" "Good, the cert is skipped." "$NOTIFY_HOOK" "$RENEW_SKIP" + fi + fi return "$RENEW_SKIP" fi - if [ "$IN_CRON" = "1" ] && [ -z "$Le_CertCreateTime" ]; then + if [ "$_ACME_IN_CRON" = "1" ] && [ -z "$Le_CertCreateTime" ]; then _info "Skip invalid cert for: $Le_Domain" - return 0 + return $RENEW_SKIP fi - IS_RENEW="1" - issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" + _ACME_IS_RENEW="1" + Le_ReloadCmd="$(_readdomainconf Le_ReloadCmd)" + Le_PreHook="$(_readdomainconf Le_PreHook)" + Le_PostHook="$(_readdomainconf Le_PostHook)" + Le_RenewHook="$(_readdomainconf Le_RenewHook)" + Le_Preferred_Chain="$(_readdomainconf Le_Preferred_Chain)" + # When renewing from an old version, the empty Le_Keylength means 2048. + # Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over + # time but an empty value implies 2048 specifically. + Le_Keylength="$(_readdomainconf Le_Keylength)" + if [ -z "$Le_Keylength" ]; then + Le_Keylength=2048 + fi + issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" res="$?" if [ "$res" != "0" ]; then return "$res" @@ -4369,22 +5429,45 @@ renew() { res="$?" fi - IS_RENEW="" + _ACME_IS_RENEW="" + if [ -z "$_ACME_IN_RENEWALL" ]; then + if [ "$res" = "0" ]; then + if [ $_set_level -ge $NOTIFY_LEVEL_RENEW ]; then + _send_notify "Renew $d success" "Good, the cert is renewed." "$NOTIFY_HOOK" 0 + fi + else + if [ $_set_level -ge $NOTIFY_LEVEL_ERROR ]; then + _send_notify "Renew $d error" "There is an error." "$NOTIFY_HOOK" 1 + fi + fi + fi return "$res" } -#renewAll [stopRenewOnError] +#renewAll [stopRenewOnError] [server] renewAll() { _initpath + _clearCA _stopRenewOnError="$1" _debug "_stopRenewOnError" "$_stopRenewOnError" - _ret="0" + _server="$2" + _debug "_server" "$_server" + + _ret="0" + _success_msg="" + _error_msg="" + _skipped_msg="" + _error_level=$NOTIFY_LEVEL_SKIP + _notify_code=$RENEW_SKIP + _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} + _debug "_set_level" "$_set_level" + export _ACME_IN_RENEWALL=1 for di in "${CERT_HOME}"/*.*/; do _debug di "$di" if ! [ -d "$di" ]; then - _debug "Not directory, skip: $di" + _debug "Not a directory, skip: $di" continue fi d=$(basename "$di") @@ -4394,22 +5477,91 @@ renewAll() { _isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2) d=$(echo "$d" | cut -d "$ECC_SEP" -f 1) fi - renew "$d" "$_isEcc" + renew "$d" "$_isEcc" "$_server" ) rc="$?" _debug "Return code: $rc" - if [ "$rc" != "0" ]; then - if [ "$rc" = "$RENEW_SKIP" ]; then - _info "Skipped $d" - elif [ "$_stopRenewOnError" ]; then + if [ "$rc" = "0" ]; then + if [ $_error_level -gt $NOTIFY_LEVEL_RENEW ]; then + _error_level="$NOTIFY_LEVEL_RENEW" + _notify_code=0 + fi + + if [ $_set_level -ge $NOTIFY_LEVEL_RENEW ]; then + if [ "$NOTIFY_MODE" = "$NOTIFY_MODE_CERT" ]; then + _send_notify "Renew $d success" "Good, the cert is renewed." "$NOTIFY_HOOK" 0 + fi + fi + + _success_msg="${_success_msg} $d +" + elif [ "$rc" = "$RENEW_SKIP" ]; then + if [ $_error_level -gt $NOTIFY_LEVEL_SKIP ]; then + _error_level="$NOTIFY_LEVEL_SKIP" + _notify_code=$RENEW_SKIP + fi + + if [ $_set_level -ge $NOTIFY_LEVEL_SKIP ]; then + if [ "$NOTIFY_MODE" = "$NOTIFY_MODE_CERT" ]; then + _send_notify "Renew $d skipped" "Good, the cert is skipped." "$NOTIFY_HOOK" "$RENEW_SKIP" + fi + fi + + _info "Skipped $d" + _skipped_msg="${_skipped_msg} $d +" + else + if [ $_error_level -gt $NOTIFY_LEVEL_ERROR ]; then + _error_level="$NOTIFY_LEVEL_ERROR" + _notify_code=1 + fi + + if [ $_set_level -ge $NOTIFY_LEVEL_ERROR ]; then + if [ "$NOTIFY_MODE" = "$NOTIFY_MODE_CERT" ]; then + _send_notify "Renew $d error" "There is an error." "$NOTIFY_HOOK" 1 + fi + fi + + _error_msg="${_error_msg} $d +" + if [ "$_stopRenewOnError" ]; then _err "Error renew $d, stop now." - return "$rc" + _ret="$rc" + break else _ret="$rc" _err "Error renew $d." fi fi done + _debug _error_level "$_error_level" + _debug _set_level "$_set_level" + if [ $_error_level -le $_set_level ]; then + if [ -z "$NOTIFY_MODE" ] || [ "$NOTIFY_MODE" = "$NOTIFY_MODE_BULK" ]; then + _msg_subject="Renew" + if [ "$_error_msg" ]; then + _msg_subject="${_msg_subject} Error" + _msg_data="Error certs: +${_error_msg} +" + fi + if [ "$_success_msg" ]; then + _msg_subject="${_msg_subject} Success" + _msg_data="${_msg_data}Success certs: +${_success_msg} +" + fi + if [ "$_skipped_msg" ]; then + _msg_subject="${_msg_subject} Skipped" + _msg_data="${_msg_data}Skipped certs: +${_skipped_msg} +" + fi + + _send_notify "$_msg_subject" "$_msg_data" "$NOTIFY_HOOK" "$_notify_code" + fi + fi + return "$_ret" } @@ -4418,7 +5570,7 @@ signcsr() { _csrfile="$1" _csrW="$2" if [ -z "$_csrfile" ] || [ -z "$_csrW" ]; then - _usage "Usage: $PROJECT_ENTRY --signcsr --csr mycsr.csr -w /path/to/webroot/a.com/ " + _usage "Usage: $PROJECT_ENTRY --sign-csr --csr --webroot " return 1 fi @@ -4432,6 +5584,7 @@ signcsr() { _renew_hook="${10}" _local_addr="${11}" _challenge_alias="${12}" + _preferred_chain="${13}" _csrsubj=$(_readSubjectFromCSR "$_csrfile") if [ "$?" != "0" ]; then @@ -4469,16 +5622,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" } @@ -4486,17 +5636,20 @@ showcsr() { _csrfile="$1" _csrd="$2" if [ -z "$_csrfile" ] && [ -z "$_csrd" ]; then - _usage "Usage: $PROJECT_ENTRY --showcsr --csr mycsr.csr" + _usage "Usage: $PROJECT_ENTRY --show-csr --csr " return 1 fi _initpath _csrsubj=$(_readSubjectFromCSR "$_csrfile") - if [ "$?" != "0" ] || [ -z "$_csrsubj" ]; then + if [ "$?" != "0" ]; then _err "Can not read subject from csr: $_csrfile" return 1 fi + if [ -z "$_csrsubj" ]; then + _info "The Subject is empty" + fi _info "Subject=$_csrsubj" @@ -4517,37 +5670,44 @@ showcsr() { _info "KeyLength=$_csrkeylength" } +#listraw domain list() { _raw="$1" + _domain="$2" _initpath _sep="|" if [ "$_raw" ]; then - printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Created${_sep}Renew" + if [ -z "$_domain" ]; then + printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}CA${_sep}Created${_sep}Renew" + fi for di in "${CERT_HOME}"/*.*/; do - if ! [ -d "$di" ]; then - _debug "Not directory, skip: $di" - continue - fi d=$(basename "$di") _debug d "$d" ( if _endswith "$d" "$ECC_SUFFIX"; then - _isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2) + _isEcc="ecc" d=$(echo "$d" | cut -d "$ECC_SEP" -f 1) fi - _initpath "$d" "$_isEcc" + DOMAIN_CONF="$di/$d.conf" if [ -f "$DOMAIN_CONF" ]; then . "$DOMAIN_CONF" - printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr" + _ca="$(_getCAShortName "$Le_API")" + if [ -z "$_domain" ]; then + printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr" + else + if [ "$_domain" = "$d" ]; then + cat "$DOMAIN_CONF" + fi + fi fi ) done else if _exists column; then - list "raw" | column -t -s "$_sep" + list "raw" "$_domain" | column -t -s "$_sep" else - list "raw" | tr "$_sep" '\t' + list "raw" "$_domain" | tr "$_sep" '\t' fi fi @@ -4558,7 +5718,7 @@ _deploy() { _hooks="$2" for _d_api in $(echo "$_hooks" | tr ',' " "); do - _deployApi="$(_findHook "$_d" deploy "$_d_api")" + _deployApi="$(_findHook "$_d" $_SUB_FOLDER_DEPLOY "$_d_api")" if [ -z "$_deployApi" ]; then _err "The deploy hook $_d_api is not found." return 1 @@ -4596,7 +5756,7 @@ deploy() { _hooks="$2" _isEcc="$3" if [ -z "$_hooks" ]; then - _usage "Usage: $PROJECT_ENTRY --deploy -d domain.com --deploy-hook cpanel [--ecc] " + _usage "Usage: $PROJECT_ENTRY --deploy --domain --deploy-hook [--ecc] " return 1 fi @@ -4607,6 +5767,7 @@ deploy() { return 1 fi + _debug2 DOMAIN_CONF "$DOMAIN_CONF" . "$DOMAIN_CONF" _savedomainconf Le_DeployHook "$_hooks" @@ -4617,7 +5778,7 @@ deploy() { installcert() { _main_domain="$1" if [ -z "$_main_domain" ]; then - _usage "Usage: $PROJECT_ENTRY --installcert -d domain.com [--ecc] [--cert-file cert-file-path] [--key-file key-file-path] [--ca-file ca-cert-file-path] [ --reloadCmd reloadCmd] [--fullchain-file fullchain-path]" + _usage "Usage: $PROJECT_ENTRY --install-cert --domain [--ecc] [--cert-file ] [--key-file ] [--ca-file ] [ --reloadcmd ] [--fullchain-file ]" return 1 fi @@ -4638,9 +5799,10 @@ installcert() { _savedomainconf "Le_RealCertPath" "$_real_cert" _savedomainconf "Le_RealCACertPath" "$_real_ca" _savedomainconf "Le_RealKeyPath" "$_real_key" - _savedomainconf "Le_ReloadCmd" "$_reload_cmd" + _savedomainconf "Le_ReloadCmd" "$_reload_cmd" "base64" _savedomainconf "Le_RealFullChainPath" "$_real_fullchain" - + export Le_ForceNewDomainKey="$(_readdomainconf Le_ForceNewDomainKey)" + export Le_Next_Domain_Key _installcert "$_main_domain" "$_real_cert" "$_real_key" "$_real_ca" "$_real_fullchain" "$_reload_cmd" } @@ -4674,45 +5836,54 @@ _installcert() { mkdir -p "$_backup_path" if [ "$_real_cert" ]; then - _info "Installing cert to:$_real_cert" - if [ -f "$_real_cert" ] && [ ! "$IS_RENEW" ]; then + _info "Installing cert to: $_real_cert" + if [ -f "$_real_cert" ] && [ ! "$_ACME_IS_RENEW" ]; then cp "$_real_cert" "$_backup_path/cert.bak" fi - cat "$CERT_PATH" >"$_real_cert" || return 1 + if [ "$CERT_PATH" != "$_real_cert" ]; then + cat "$CERT_PATH" >"$_real_cert" || return 1 + fi 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 else - if [ -f "$_real_ca" ] && [ ! "$IS_RENEW" ]; then + if [ -f "$_real_ca" ] && [ ! "$_ACME_IS_RENEW" ]; then cp "$_real_ca" "$_backup_path/ca.bak" fi - cat "$CA_CERT_PATH" >"$_real_ca" || return 1 + if [ "$CA_CERT_PATH" != "$_real_ca" ]; then + cat "$CA_CERT_PATH" >"$_real_ca" || return 1 + fi fi fi if [ "$_real_key" ]; then - _info "Installing key to:$_real_key" - if [ -f "$_real_key" ] && [ ! "$IS_RENEW" ]; then + _info "Installing key to: $_real_key" + if [ -f "$_real_key" ] && [ ! "$_ACME_IS_RENEW" ]; then cp "$_real_key" "$_backup_path/key.bak" fi - if [ -f "$_real_key" ]; then - cat "$CERT_KEY_PATH" >"$_real_key" || return 1 - else - cat "$CERT_KEY_PATH" >"$_real_key" || return 1 - chmod 600 "$_real_key" + if [ "$CERT_KEY_PATH" != "$_real_key" ]; then + if [ -f "$_real_key" ]; then + cat "$CERT_KEY_PATH" >"$_real_key" || return 1 + else + touch "$_real_key" || return 1 + chmod 600 "$_real_key" + cat "$CERT_KEY_PATH" >"$_real_key" || return 1 + fi fi fi if [ "$_real_fullchain" ]; then - _info "Installing full chain to:$_real_fullchain" - if [ -f "$_real_fullchain" ] && [ ! "$IS_RENEW" ]; then + _info "Installing full chain to: $_real_fullchain" + if [ -f "$_real_fullchain" ] && [ ! "$_ACME_IS_RENEW" ]; then cp "$_real_fullchain" "$_backup_path/fullchain.bak" fi - cat "$CERT_FULLCHAIN_PATH" >"$_real_fullchain" || return 1 + if [ "$_real_fullchain" != "$CERT_FULLCHAIN_PATH" ]; then + cat "$CERT_FULLCHAIN_PATH" >"$_real_fullchain" || return 1 + fi fi if [ "$_reload_cmd" ]; then @@ -4722,7 +5893,9 @@ _installcert() { export CERT_KEY_PATH export CA_CERT_PATH export CERT_FULLCHAIN_PATH - export Le_Domain + export Le_Domain="$_main_domain" + export Le_ForceNewDomainKey + export Le_Next_Domain_Key cd "$DOMAIN_PATH" && eval "$_reload_cmd" ); then _info "$(__green "Reload success")" @@ -4733,46 +5906,125 @@ _installcert() { } +__read_password() { + unset _pp + prompt="Enter Password:" + while IFS= read -p "$prompt" -r -s -n 1 char; do + if [ "$char" = $'\0' ]; then + break + fi + prompt='*' + _pp="$_pp$char" + done + echo "$_pp" +} + +_install_win_taskscheduler() { + _lesh="$1" + _centry="$2" + _randomminute="$3" + if ! _exists cygpath; then + _err "cygpath not found" + return 1 + fi + if ! _exists schtasks; then + _err "schtasks.exe is not found, are you on Windows?" + return 1 + fi + _winbash="$(cygpath -w $(which bash))" + _debug _winbash "$_winbash" + if [ -z "$_winbash" ]; then + _err "can not find bash path" + return 1 + fi + _myname="$(whoami)" + _debug "_myname" "$_myname" + if [ -z "$_myname" ]; then + _err "can not find my user name" + return 1 + fi + _debug "_lesh" "$_lesh" + + _info "To install scheduler task in your Windows account, you must input your windows password." + _info "$PROJECT_NAME doesn't save your password." + _info "Please input your Windows password for: $(__green "$_myname")" + _password="$(__read_password)" + #SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_randomminute" '/RU' "$_myname" '/RP' "$_password" '/TR' "$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'" >/dev/null + echo SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_randomminute" '/RU' "$_myname" '/RP' "$_password" '/TR' "\"$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'\"" | cmd.exe >/dev/null + echo + +} + +_uninstall_win_taskscheduler() { + if ! _exists schtasks; then + _err "schtasks.exe is not found, are you on Windows?" + return 1 + fi + if ! echo SCHTASKS /query /tn "$_WINDOWS_SCHEDULER_NAME" | cmd.exe >/dev/null; then + _debug "scheduler $_WINDOWS_SCHEDULER_NAME is not found." + else + _info "Removing $_WINDOWS_SCHEDULER_NAME" + echo SCHTASKS /delete /f /tn "$_WINDOWS_SCHEDULER_NAME" | cmd.exe >/dev/null + fi +} + #confighome installcronjob() { _c_home="$1" _initpath _CRONTAB="crontab" + if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY" ]; then + lesh="\"$LE_WORKING_DIR\"/$PROJECT_ENTRY" + else + _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\" " + fi + _t=$(_time) + random_minute=$(_math $_t % 60) + random_hour=$(_math $_t / 60 % 24) + if ! _exists "$_CRONTAB" && _exists "fcrontab"; then _CRONTAB="fcrontab" fi + if ! _exists "$_CRONTAB"; then + if _exists cygpath && _exists schtasks.exe; then + _info "It seems you are on Windows, let's install Windows scheduler task." + if _install_win_taskscheduler "$lesh" "$_c_entry" "$random_minute"; then + _info "Install Windows scheduler task success." + return 0 + else + _err "Install Windows scheduler task failed." + return 1 + fi + fi _err "crontab/fcrontab doesn't exist, so, we can not install cron jobs." _err "All your certs will not be renewed automatically." _err "You must add your own cron job to call '$PROJECT_ENTRY --cron' everyday." return 1 fi - _info "Installing cron job" if ! $_CRONTAB -l | grep "$PROJECT_ENTRY --cron"; then - 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 - fi - - if [ "$_c_home" ]; then - _c_entry="--config-home \"$_c_home\" " - fi - _t=$(_time) - random_minute=$(_math $_t % 60) if _exists uname && uname -a | grep SunOS >/dev/null; then - $_CRONTAB -l | { - cat - echo "$random_minute 0 * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" - } | $_CRONTAB -- + _CRONTAB_STDIN="$_CRONTAB --" else - $_CRONTAB -l | { - cat - echo "$random_minute 0 * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" - } | $_CRONTAB - + _CRONTAB_STDIN="$_CRONTAB -" fi + $_CRONTAB -l | { + cat + echo "$random_minute $random_hour * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" + } | $_CRONTAB_STDIN fi if [ "$?" != "0" ]; then _err "Install cron job failed. You need to manually renew your certs." @@ -4789,12 +6041,22 @@ uninstallcronjob() { fi if ! _exists "$_CRONTAB"; then + if _exists cygpath && _exists schtasks.exe; then + _info "It seems you are on Windows, let's uninstall Windows scheduler task." + if _uninstall_win_taskscheduler; then + _info "Uninstall Windows scheduler task success." + return 0 + else + _err "Uninstall Windows scheduler task failed." + return 1 + fi + fi return fi _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 - @@ -4810,15 +6072,19 @@ uninstallcronjob() { } +#domain isECC revokeReason revoke() { Le_Domain="$1" if [ -z "$Le_Domain" ]; then - _usage "Usage: $PROJECT_ENTRY --revoke -d domain.com [--ecc]" + _usage "Usage: $PROJECT_ENTRY --revoke --domain [--ecc]" return 1 fi _isEcc="$2" - + _reason="$3" + if [ -z "$_reason" ]; then + _reason="0" + fi _initpath "$Le_Domain" "$_isEcc" if [ ! -f "$DOMAIN_CONF" ]; then _err "$Le_Domain is not a issued domain, skip." @@ -4830,6 +6096,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 @@ -4839,19 +6122,32 @@ revoke() { _initAPI - if [ "$ACME_VERSION" = "2" ]; then - data="{\"certificate\": \"$cert\"}" - else - data="{\"resource\": \"revoke-cert\", \"certificate\": \"$cert\"}" - fi + data="{\"certificate\": \"$cert\",\"reason\":$_reason}" + uri="${ACME_REVOKE_CERT}" + _info "Try account key first." + if _send_signed_request "$uri" "$data" "" "$ACCOUNT_KEY_PATH"; then + if [ -z "$response" ]; then + _info "Revoke success." + rm -f "$CERT_PATH" + cat "$CERT_KEY_PATH" >"$CERT_KEY_PATH.revoked" + cat "$CSR_PATH" >"$CSR_PATH.revoked" + return 0 + else + _err "Revoke error." + _debug "$response" + fi + fi + if [ -f "$CERT_KEY_PATH" ]; then - _info "Try domain key first." + _info "Try domain key." if _send_signed_request "$uri" "$data" "" "$CERT_KEY_PATH"; then if [ -z "$response" ]; then _info "Revoke success." rm -f "$CERT_PATH" + cat "$CERT_KEY_PATH" >"$CERT_KEY_PATH.revoked" + cat "$CSR_PATH" >"$CSR_PATH.revoked" return 0 else _err "Revoke error by domain key." @@ -4859,20 +6155,7 @@ revoke() { fi fi else - _info "Domain key file doesn't exists." - fi - - _info "Try account key." - - if _send_signed_request "$uri" "$data" "" "$ACCOUNT_KEY_PATH"; then - if [ -z "$response" ]; then - _info "Revoke success." - rm -f "$CERT_PATH" - return 0 - else - _err "Revoke error." - _debug "$response" - fi + _info "Domain key file doesn't exist." fi return 1 } @@ -4881,7 +6164,7 @@ revoke() { remove() { Le_Domain="$1" if [ -z "$Le_Domain" ]; then - _usage "Usage: $PROJECT_ENTRY --remove -d domain.com [--ecc]" + _usage "Usage: $PROJECT_ENTRY --remove --domain [--ecc]" return 1 fi @@ -4912,70 +6195,72 @@ 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" | tr -d '\r\n' | _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 ! response="$(_get "$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" | _egrep_o "{ *\"type\":\"[^\"]*\", *\"status\": *\"valid\", *\"$_URL_NAME\"[^}]*")" + _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" - entry="$(printf "%s\n" "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" + vtype="$(_getIdType "$_d_domain")" + entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$entry" ]; then _err "Error, can not get domain token $d" return 1 fi - token="$(printf "%s\n" "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" + token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" _debug token "$token" - uri="$(printf "%s\n" "$entry" | _egrep_o "\"$_URL_NAME\":\"[^\"]*" | cut -d : -f 2,3 | tr -d '"')" + uri="$(echo "$entry" | _egrep_o "\"$_URL_NAME\":\"[^\"]*" | cut -d : -f 2,3 | tr -d '"')" _debug uri "$uri" keyauthorization="$token.$thumbprint" @@ -4997,11 +6282,11 @@ _deactivate() { break fi - _vtype="$(printf "%s\n" "$entry" | _egrep_o '"type": *"[^"]*"' | cut -d : -f 2 | tr -d '"')" + _vtype="$(echo "$entry" | _egrep_o '"type": *"[^"]*"' | cut -d : -f 2 | tr -d '"')" _debug _vtype "$_vtype" _info "Found $_vtype" - uri="$(printf "%s\n" "$entry" | _egrep_o "\"$_URL_NAME\":\"[^\"]*" | cut -d : -f 2,3 | tr -d '"')" + uri="$(echo "$entry" | _egrep_o "\"$_URL_NAME\":\"[^\"]*\"" | tr -d '" ' | cut -d : -f 2-)" _debug uri "$uri" if [ "$_d_type" ] && [ "$_d_type" != "$_vtype" ]; then @@ -5011,11 +6296,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." @@ -5041,7 +6322,7 @@ deactivate() { _initAPI _debug _d_domain_list "$_d_domain_list" if [ -z "$(echo $_d_domain_list | cut -d , -f 1)" ]; then - _usage "Usage: $PROJECT_ENTRY --deactivate -d domain.com [-d domain.com]" + _usage "Usage: $PROJECT_ENTRY --deactivate --domain [--domain ...]" return 1 fi for _d_dm in $(echo "$_d_domain_list" | tr ',' ' '); do @@ -5116,13 +6397,17 @@ _precheck() { if [ -z "$_nocron" ]; then if ! _exists "crontab" && ! _exists "fcrontab"; then - _err "It is recommended to install crontab first. try to install 'cron, crontab, crontabs or vixie-cron'." - _err "We need to set cron job to renew the certs automatically." - _err "Otherwise, your certs will not be able to be renewed automatically." - if [ -z "$FORCE" ]; then - _err "Please add '--force' and try install again to go without crontab." - _err "./$PROJECT_ENTRY --install --force" - return 1 + if _exists cygpath && _exists schtasks.exe; then + _info "It seems you are on Windows, we will install Windows scheduler task." + else + _err "It is recommended to install crontab first. try to install 'cron, crontab, crontabs or vixie-cron'." + _err "We need to set cron job to renew the certs automatically." + _err "Otherwise, your certs will not be able to be renewed automatically." + if [ -z "$FORCE" ]; then + _err "Please add '--force' and try install again to go without crontab." + _err "./$PROJECT_ENTRY --install --force" + return 1 + fi fi fi fi @@ -5218,7 +6503,7 @@ _installalias() { } -# nocron confighome noprofile +# nocron confighome noprofile accountemail install() { if [ -z "$LE_WORKING_DIR" ]; then @@ -5228,6 +6513,8 @@ install() { _nocron="$1" _c_home="$2" _noprofile="$3" + _accountemail="$4" + if ! _initpath; then _err "Install failed." return 1 @@ -5236,7 +6523,7 @@ install() { _debug "Skip install cron job" fi - if [ "$IN_CRON" != "1" ]; then + if [ "$_ACME_IN_CRON" != "1" ]; then if ! _precheck "$_nocron"; then _err "Pre-check failed, can not install." return 1 @@ -5293,7 +6580,7 @@ install() { _info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY" - if [ "$IN_CRON" != "1" ] && [ -z "$_noprofile" ]; then + if [ "$_ACME_IN_CRON" != "1" ] && [ -z "$_noprofile" ]; then _installalias "$_c_home" fi @@ -5346,6 +6633,10 @@ install() { fi fi + if [ "$_accountemail" ]; then + _saveaccountconf "ACCOUNT_EMAIL" "$_accountemail" + fi + _saveaccountconf "UPGRADE_HASH" "$(_getUpgradeHash)" _info OK } @@ -5391,7 +6682,7 @@ _uninstallalias() { } cron() { - export IN_CRON=1 + export _ACME_IN_CRON=1 _initpath _info "$(__green "===Starting cron===")" if [ "$AUTO_UPGRADE" = "1" ]; then @@ -5412,7 +6703,7 @@ cron() { fi renewAll _ret="$?" - IN_CRON="" + _ACME_IN_CRON="" _info "$(__green "===End cron===")" exit $_ret } @@ -5422,118 +6713,294 @@ version() { echo "v$VER" } +# subject content hooks code +_send_notify() { + _nsubject="$1" + _ncontent="$2" + _nhooks="$3" + _nerror="$4" + + if [ "$NOTIFY_LEVEL" = "$NOTIFY_LEVEL_DISABLE" ]; then + _debug "The NOTIFY_LEVEL is $NOTIFY_LEVEL, disabled, just return." + return 0 + fi + + if [ -z "$_nhooks" ]; then + _debug "The NOTIFY_HOOK is empty, just return." + return 0 + fi + + _nsource="$NOTIFY_SOURCE" + if [ -z "$_nsource" ]; then + _nsource="$(hostname)" + fi + + _nsubject="$_nsubject by $_nsource" + + _send_err=0 + for _n_hook in $(echo "$_nhooks" | tr ',' " "); do + _n_hook_file="$(_findHook "" $_SUB_FOLDER_NOTIFY "$_n_hook")" + _info "Sending via: $_n_hook" + _debug "Found $_n_hook_file for $_n_hook" + if [ -z "$_n_hook_file" ]; then + _err "Can not find the hook file for $_n_hook" + continue + fi + if ! ( + if ! . "$_n_hook_file"; then + _err "Load file $_n_hook_file error. Please check your api file and try again." + return 1 + fi + + d_command="${_n_hook}_send" + if ! _exists "$d_command"; then + _err "It seems that your api file is not correct, it must have a function named: $d_command" + return 1 + fi + + if ! $d_command "$_nsubject" "$_ncontent" "$_nerror"; then + _err "Error send message by $d_command" + return 1 + fi + + return 0 + ); then + _err "Set $_n_hook_file error." + _send_err=1 + else + _info "$_n_hook $(__green Success)" + fi + done + return $_send_err + +} + +# hook +_set_notify_hook() { + _nhooks="$1" + + _test_subject="Hello, this is a notification from $PROJECT_NAME" + _test_content="If you receive this message, your notification works." + + _send_notify "$_test_subject" "$_test_content" "$_nhooks" 0 + +} + +#[hook] [level] [mode] +setnotify() { + _nhook="$1" + _nlevel="$2" + _nmode="$3" + _nsource="$4" + + _initpath + + if [ -z "$_nhook$_nlevel$_nmode$_nsource" ]; then + _usage "Usage: $PROJECT_ENTRY --set-notify [--notify-hook ] [--notify-level <0|1|2|3>] [--notify-mode <0|1>] [--notify-source ]" + _usage "$_NOTIFY_WIKI" + return 1 + fi + + if [ "$_nlevel" ]; then + _info "Set notify level to: $_nlevel" + export "NOTIFY_LEVEL=$_nlevel" + _saveaccountconf "NOTIFY_LEVEL" "$NOTIFY_LEVEL" + fi + + if [ "$_nmode" ]; then + _info "Set notify mode to: $_nmode" + export "NOTIFY_MODE=$_nmode" + _saveaccountconf "NOTIFY_MODE" "$NOTIFY_MODE" + fi + + if [ "$_nsource" ]; then + _info "Set notify source to: $_nsource" + export "NOTIFY_SOURCE=$_nsource" + _saveaccountconf "NOTIFY_SOURCE" "$NOTIFY_SOURCE" + fi + + if [ "$_nhook" ]; then + _info "Set notify hook to: $_nhook" + if [ "$_nhook" = "$NO_VALUE" ]; then + _info "Clear notify hook" + _clearaccountconf "NOTIFY_HOOK" + else + if _set_notify_hook "$_nhook"; then + export NOTIFY_HOOK="$_nhook" + _saveaccountconf "NOTIFY_HOOK" "$NOTIFY_HOOK" + return 0 + else + _err "Can not set notify hook to: $_nhook" + return 1 + fi + fi + fi + +} + showhelp() { _initpath version - echo "Usage: $PROJECT_ENTRY command ...[parameters].... + echo "Usage: $PROJECT_ENTRY ... [parameters ...] Commands: - --help, -h Show this help message. - --version, -v Show version info. + -h, --help Show this help message. + -v, --version Show version info. --install Install $PROJECT_NAME to your system. --uninstall Uninstall $PROJECT_NAME, and uninstall the cron job. --upgrade Upgrade $PROJECT_NAME to the latest code from $PROJECT. --issue Issue a cert. - --signcsr Issue a cert from an existing csr. --deploy Deploy the cert to your server. - --install-cert Install the issued cert to apache/nginx or any other server. - --renew, -r Renew a cert. + -i, --install-cert Install the issued cert to apache/nginx or any other server. + -r, --renew Renew a cert. --renew-all Renew all the certs. --revoke Revoke a cert. --remove Remove the cert from list of certs known to $PROJECT_NAME. --list List all the certs. - --showcsr Show the content of a csr. - --install-cronjob Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job. - --uninstall-cronjob Uninstall the cron job. The 'uninstall' command can do this automatically. - --cron Run cron job to renew all the certs. - --toPkcs Export the certificate and key to a pfx file. - --toPkcs8 Convert to pkcs8 format. + --info Show the $PROJECT_NAME configs, or the configs for a domain with [-d domain] parameter. + --to-pkcs12 Export the certificate and key to a pfx file. + --to-pkcs8 Convert to pkcs8 format. + --sign-csr Issue a cert from an existing csr. + --show-csr Show the content of a csr. + -ccr, --create-csr Create CSR, professional use. + --create-domain-key Create an domain private key, professional use. --update-account Update account info. --register-account Register account key. --deactivate-account Deactivate the account. --create-account-key Create an account private key, professional use. - --create-domain-key Create an domain private key, professional use. - --createCSR, -ccsr Create CSR , professional use. + --install-cronjob Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job. + --uninstall-cronjob Uninstall the cron job. The 'uninstall' command can do this automatically. + --cron Run cron job to renew all the certs. + --set-notify Set the cron notification hook, level or mode. --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: - --domain, -d domain.tld Specifies a domain, used to issue, renew or revoke etc. - --challenge-alias domain.tld The challenge domain alias for DNS alias mode: $_DNS_ALIAS_WIKI - --domain-alias domain.tld The domain alias for DNS alias mode: $_DNS_ALIAS_WIKI - --force, -f Used to force to install or force to renew a cert immediately. - --staging, --test Use staging server, just for test. - --debug Output debug info. - --output-insecure Output all the sensitive messages. By default all the credentials/sensitive messages are hidden from the output/debug/log for secure. - --webroot, -w /path/to/webroot Specifies the web root folder for web root mode. - --standalone Use standalone mode. - --stateless Use stateless mode, see: $_STATELESS_WIKI - --apache Use apache mode. - --dns [dns_cf|dns_dp|dns_cx|/path/to/api/file] Use dns mode or dns api. - --dnssleep [$DEFAULT_DNS_SLEEP] The time in seconds to wait for all the txt records to take effect in dns api mode. Default $DEFAULT_DNS_SLEEP seconds. + -d, --domain Specifies a domain, used to issue, renew or revoke etc. + --challenge-alias The challenge domain alias for DNS alias mode. + See: $_DNS_ALIAS_WIKI + + --domain-alias The domain alias for DNS alias mode. + See: $_DNS_ALIAS_WIKI + + --preferred-chain If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. + If no match, the default offered chain will be used. (default: empty) + See: $_PREFERRED_CHAIN_WIKI + + --valid-to Request the NotAfter field of the cert. + See: $_VALIDITY_WIKI + --valid-from Request the NotBefore field of the cert. + See: $_VALIDITY_WIKI + + -f, --force Force install, force cert renewal or override sudo restrictions. + --staging, --test Use staging server, for testing. + --debug [0|1|2|3] Output debug info. Defaults to $DEBUG_LEVEL_DEFAULT if argument is omitted. + --output-insecure Output all the sensitive messages. + By default all the credentials/sensitive messages are hidden from the output/debug/log for security. + -w, --webroot Specifies the web root folder for web root mode. + --standalone Use standalone mode. + --alpn Use standalone alpn mode. + --stateless Use stateless mode. + See: $_STATELESS_WIKI + + --apache Use apache mode. + --dns [dns_hook] Use dns manual mode or dns api. Defaults to manual mode when argument is omitted. + See: $_DNS_API_WIKI + + --dnssleep The time in seconds to wait for all the txt records to propagate in dns api mode. + It's not necessary to use this by default, $PROJECT_NAME polls dns status by DOH automatically. + -k, --keylength Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384, ec-521. + -ak, --accountkeylength Specifies the account key length: 2048, 3072, 4096 + --log [file] Specifies the log file. Defaults to \"$DEFAULT_LOG_FILE\" if argument is omitted. + --log-level <1|2> Specifies the log level, default is $DEFAULT_LOG_LEVEL. + --syslog <0|3|6|7> Syslog level, 0: disable syslog, 3: error, 6: info, 7: debug. + --eab-kid Key Identifier for External Account Binding. + --eab-hmac-key HMAC key for External Account Binding. - --keylength, -k [2048] Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384. - --accountkeylength, -ak [2048] Specifies the account key length. - --log [/path/to/logfile] Specifies the log file. The default is: \"$DEFAULT_LOG_FILE\" if you don't give a file path here. - --log-level 1|2 Specifies the log level, default is 1. - --syslog [0|3|6|7] Syslog level, 0: disable syslog, 3: error, 6: info, 7: debug. These parameters are to install the cert to nginx/apache or any other server after issue/renew a cert: - --cert-file After issue/renew, the cert will be copied to this path. - --key-file After issue/renew, the key will be copied to this path. - --ca-file After issue/renew, the intermediate cert will be copied to this path. - --fullchain-file After issue/renew, the fullchain cert will be copied to this path. + --cert-file Path to copy the cert file to after issue/renew. + --key-file Path to copy the key file to after issue/renew. + --ca-file Path to copy the intermediate cert file to after issue/renew. + --fullchain-file Path to copy the fullchain cert file to after issue/renew. + --reloadcmd Command to execute after issue/renew to reload the server. - --reloadcmd \"service nginx reload\" After issue/renew, it's used to reload the server. + --server ACME Directory Resource URI. (default: $DEFAULT_CA) + See: $_SERVER_WIKI - --server SERVER ACME Directory Resource URI. (default: https://acme-v01.api.letsencrypt.org/directory) - --accountconf Specifies a customized account config file. - --home Specifies the home dir for $PROJECT_NAME . - --cert-home Specifies the home dir to save all the certs, only valid for '--install' command. - --config-home Specifies the home dir to save all the configurations. - --useragent Specifies the user agent string. it will be saved for future use too. - --accountemail Specifies the account email, only valid for the '--install' and '--update-account' command. - --accountkey Specifies the account key path, only valid for the '--install' command. - --days Specifies the days to renew the cert when using '--issue' command. The max value is $MAX_RENEW days. - --httpport Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer. - --local-address Specifies the standalone/tls server listening address, in case you have multiple ip addresses. + --accountconf Specifies a customized account config file. + --home Specifies the home dir for $PROJECT_NAME. + --cert-home Specifies the home dir to save all the certs, only valid for '--install' command. + --config-home Specifies the home dir to save all the configurations. + --useragent Specifies the user agent string. it will be saved for future use too. + -m, --email Specifies the account email, only valid for the '--install' and '--update-account' command. + --accountkey Specifies the account key path, only valid for the '--install' command. + --days Specifies the days to renew the cert when using '--issue' command. The default value is $DEFAULT_RENEW days. + --httpport Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer. + --tlsport Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer. + --local-address Specifies the standalone/tls server listening address, in case you have multiple ip addresses. --listraw Only used for '--list' command, list the certs in raw format. - --stopRenewOnError, -se Only valid for '--renew-all' command. Stop if one cert has error in renewal. + -se, --stop-renew-on-error Only valid for '--renew-all' command. Stop if one cert has error in renewal. --insecure Do not check the server certificate, in some devices, the api server's certificate may not be trusted. - --ca-bundle Specifies the path to the CA certificate bundle to verify api server's certificate. - --ca-path Specifies directory containing CA certificates in PEM format, used by wget or curl. - --nocron Only valid for '--install' command, which means: do not install the default cron job. In this case, the certs will not be renewed automatically. + --ca-bundle Specifies the path to the CA certificate bundle to verify api server's certificate. + --ca-path Specifies directory containing CA certificates in PEM format, used by wget or curl. + --no-cron Only valid for '--install' command, which means: do not install the default cron job. + In this case, the certs will not be renewed automatically. + --no-profile Only valid for '--install' command, which means: do not install aliases to user profile. --no-color Do not output color text. --force-color Force output of color text. Useful for non-interactive use with the aha tool for HTML E-Mails. - --ecc Specifies to use the ECC cert. Valid for '--install-cert', '--renew', '--revoke', '--toPkcs' and '--createCSR' - --csr Specifies the input csr. - --pre-hook Command to be run before obtaining any certificates. - --post-hook Command to be run after attempting to obtain/renew certificates. No matter the obtain/renew is success or failed. - --renew-hook Command to be run once for each successfully renewed certificate. - --deploy-hook The hook file to deploy cert - --ocsp-must-staple, --ocsp Generate ocsp must Staple extension. - --always-force-new-domain-key Generate new domain key when renewal. Otherwise, the domain key is not changed by default. - --auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. + --ecc Specifies use of the ECC cert. Only valid for '--install-cert', '--renew', '--remove ', '--revoke', + '--deploy', '--to-pkcs8', '--to-pkcs12' and '--create-csr'. + --csr Specifies the input csr. + --pre-hook Command to be run before obtaining any certificates. + --post-hook Command to be run after attempting to obtain/renew certificates. Runs regardless of whether obtain/renew succeeded or failed. + --renew-hook Command to be run after each successfully renewed certificate. + --deploy-hook The hook file to deploy cert + --ocsp, --ocsp-must-staple Generate OCSP-Must-Staple extension. + --always-force-new-domain-key Generate new domain key on renewal. Otherwise, the domain key is not changed by default. + --auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. Defaults to 1 if argument is omitted. --listen-v4 Force standalone/tls server to listen at ipv4. --listen-v6 Force standalone/tls server to listen at ipv6. - --openssl-bin Specifies a custom openssl bin location. + --openssl-bin Specifies a custom openssl bin location. --use-wget Force to use wget, if you have both curl and wget installed. - --yes-I-know-dns-manual-mode-enough-go-ahead-please Force to use dns manual mode: $_DNS_MANUAL_WIKI - --branch, -b Only valid for '--upgrade' command, specifies the branch name to upgrade to. - " + --yes-I-know-dns-manual-mode-enough-go-ahead-please Force use of dns manual mode. + See: $_DNS_MANUAL_WIKI + + -b, --branch Only valid for '--upgrade' command, specifies the branch name to upgrade to. + --notify-level <0|1|2|3> Set the notification level: Default value is $NOTIFY_LEVEL_DEFAULT. + 0: disabled, no notification will be sent. + 1: send notifications only when there is an error. + 2: send notifications when a cert is successfully renewed, or there is an error. + 3: send notifications when a cert is skipped, renewed, or error. + --notify-mode <0|1> Set notification mode. Default value is $NOTIFY_MODE_DEFAULT. + 0: Bulk mode. Send all the domain's notifications in one message(mail). + 1: Cert mode. Send a message for every single cert. + --notify-hook Set the notify hook + --notify-source Set the server name in the notification message + --revoke-reason <0-10> The reason for revocation, can be used in conjunction with the '--revoke' command. + See: $_REVOKE_WIKI + + --password Add a password to exported pfx file. Use with --to-pkcs12. + + +" } -# nocron noprofile -_installOnline() { +installOnline() { _info "Installing from online archive." - _nocron="$1" - _noprofile="$2" - if [ ! "$BRANCH" ]; then - BRANCH="master" + + _branch="$BRANCH" + if [ -z "$_branch" ]; then + _branch="master" fi - target="$PROJECT/archive/$BRANCH.tar.gz" + target="$PROJECT/archive/$_branch.tar.gz" _info "Downloading $target" - localname="$BRANCH.tar.gz" + localname="$_branch.tar.gz" if ! _get "$target" >$localname; then _err "Download error." return 1 @@ -5545,25 +7012,43 @@ _installOnline() { exit 1 fi - cd "$PROJECT_NAME-$BRANCH" + cd "$PROJECT_NAME-$_branch" chmod +x $PROJECT_ENTRY - if ./$PROJECT_ENTRY install "$_nocron" "" "$_noprofile"; then + if ./$PROJECT_ENTRY --install "$@"; then _info "Install success!" fi cd .. - rm -rf "$PROJECT_NAME-$BRANCH" + rm -rf "$PROJECT_NAME-$_branch" rm -f "$localname" ) } +_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\n\n' | grep '"sha":' | cut -d '"' -f 4 +} + +_getUpgradeHash() { + _b="$BRANCH" + if [ -z "$_b" ]; then + _b="master" + fi + _hash=$(_getRepoHash "heads/$_b") + if [ -z "$_hash" ]; then _hash=$(_getRepoHash "tags/$_b"); fi + echo $_hash +} + upgrade() { if ( _initpath + [ -z "$FORCE" ] && [ "$(_getUpgradeHash)" = "$(_readaccountconf "UPGRADE_HASH")" ] && _info "Already uptodate!" && exit 0 export LE_WORKING_DIR cd "$LE_WORKING_DIR" - _installOnline "nocron" "noprofile" + installOnline "--nocron" "--noprofile" ); then _info "Upgrade success!" exit 0 @@ -5580,12 +7065,6 @@ _processAccountConf() { _saveaccountconf "USER_AGENT" "$USER_AGENT" fi - if [ "$_accountemail" ]; then - _saveaccountconf "ACCOUNT_EMAIL" "$_accountemail" - elif [ "$ACCOUNT_EMAIL" ] && [ "$ACCOUNT_EMAIL" != "$DEFAULT_ACCOUNT_EMAIL" ]; then - _saveaccountconf "ACCOUNT_EMAIL" "$ACCOUNT_EMAIL" - fi - if [ "$_openssl_bin" ]; then _saveaccountconf "ACME_OPENSSL_BIN" "$_openssl_bin" elif [ "$ACME_OPENSSL_BIN" ] && [ "$ACME_OPENSSL_BIN" != "$DEFAULT_OPENSSL_BIN" ]; then @@ -5606,14 +7085,136 @@ _processAccountConf() { } +_checkSudo() { + if [ -z "__INTERACTIVE" ]; then + #don't check if it's not in an interactive shell + return 0 + fi + if [ "$SUDO_GID" ] && [ "$SUDO_COMMAND" ] && [ "$SUDO_USER" ] && [ "$SUDO_UID" ]; then + if [ "$SUDO_USER" = "root" ] && [ "$SUDO_UID" = "0" ]; then + #it's root using sudo, no matter it's using sudo or not, just fine + return 0 + fi + if [ -n "$SUDO_COMMAND" ]; then + #it's a normal user doing "sudo su", or `sudo -i` or `sudo -s`, or `sudo su acmeuser1` + _endswith "$SUDO_COMMAND" /bin/su || _contains "$SUDO_COMMAND" "/bin/su " || grep "^$SUDO_COMMAND\$" /etc/shells >/dev/null 2>&1 + return $? + fi + #otherwise + return 1 + fi + return 0 +} + +#server #keylength +_selectServer() { + _server="$1" + _skeylength="$2" + _server_lower="$(echo "$_server" | _lower_case)" + _sindex=0 + for snames in $CA_NAMES; do + snames="$(echo "$snames" | _lower_case)" + _sindex="$(_math $_sindex + 1)" + _debug2 "_selectServer try snames" "$snames" + for sname in $(echo "$snames" | tr ',' ' '); do + 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 + return + fi + done + done + ACME_DIRECTORY="$_server" + export ACME_DIRECTORY +} + +#url +_getCAShortName() { + caurl="$1" + if [ -z "$caurl" ]; then + #use letsencrypt as default value if the Le_API is empty + #this case can only come from the old upgrading. + caurl="$CA_LETSENCRYPT_V2" + 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 + _sindex="$(_math $_sindex + 1)" + if [ "$caurl_lower" = "$surl" ]; then + _nindex=0 + for snames in $CA_NAMES; do + _nindex="$(_math $_nindex + 1)" + if [ $_nindex -ge $_sindex ]; then + _getfield "$snames" 1 + return + fi + done + fi + done + echo "$caurl" +} + +#set default ca to $ACME_DIRECTORY +setdefaultca() { + if [ -z "$ACME_DIRECTORY" ]; then + _err "Please give a --server parameter." + return 1 + fi + _saveaccountconf "DEFAULT_ACME_SERVER" "$ACME_DIRECTORY" + _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" +} + +#domain ecc +info() { + _domain="$1" + _ecc="$2" + _initpath + if [ -z "$_domain" ]; then + _debug "Show global configs" + echo "LE_WORKING_DIR=$LE_WORKING_DIR" + echo "LE_CONFIG_HOME=$LE_CONFIG_HOME" + cat "$ACCOUNT_CONF_PATH" + else + _debug "Show domain configs" + ( + _initpath "$_domain" "$_ecc" + echo "DOMAIN_CONF=$DOMAIN_CONF" + for seg in $(cat $DOMAIN_CONF | cut -d = -f 1); do + echo "$seg=$(_readdomainconf "$seg")" + done + ) + fi +} + _process() { _CMD="" _domain="" _altdomains="$NO_VALUE" _webroot="" _challenge_alias="" - _keylength="" - _accountkeylength="" + _keylength="$DEFAULT_DOMAIN_KEY_LENGTH" + _accountkeylength="$DEFAULT_ACCOUNT_KEY_LENGTH" _cert_file="" _key_file="" _ca_file="" @@ -5635,6 +7236,7 @@ _process() { _ca_bundle="" _ca_path="" _nocron="" + _noprofile="" _ecc="" _csr="" _pre_hook="" @@ -5652,434 +7254,557 @@ _process() { _syslog="" _use_wget="" _server="" + _notify_hook="" + _notify_level="" + _notify_mode="" + _notify_source="" + _revoke_reason="" + _eab_kid="" + _eab_hmac_key="" + _preferred_chain="" + _valid_from="" + _valid_to="" while [ ${#} -gt 0 ]; do case "${1}" in - --help | -h) - showhelp - return - ;; - --version | -v) - version - return - ;; - --install) - _CMD="install" - ;; - --uninstall) - _CMD="uninstall" - ;; - --upgrade) - _CMD="upgrade" - ;; - --issue) - _CMD="issue" - ;; - --deploy) - _CMD="deploy" - ;; - --signcsr) - _CMD="signcsr" - ;; - --showcsr) - _CMD="showcsr" - ;; - --installcert | -i | --install-cert) - _CMD="installcert" - ;; - --renew | -r) - _CMD="renew" - ;; - --renewAll | --renewall | --renew-all) - _CMD="renewAll" - ;; - --revoke) - _CMD="revoke" - ;; - --remove) - _CMD="remove" - ;; - --list) - _CMD="list" - ;; - --installcronjob | --install-cronjob) - _CMD="installcronjob" - ;; - --uninstallcronjob | --uninstall-cronjob) - _CMD="uninstallcronjob" - ;; - --cron) - _CMD="cron" - ;; - --toPkcs) - _CMD="toPkcs" - ;; - --toPkcs8) - _CMD="toPkcs8" - ;; - --createAccountKey | --createaccountkey | -cak | --create-account-key) - _CMD="createAccountKey" - ;; - --createDomainKey | --createdomainkey | -cdk | --create-domain-key) - _CMD="createDomainKey" - ;; - --createCSR | --createcsr | -ccr) - _CMD="createCSR" - ;; - --deactivate) - _CMD="deactivate" - ;; - --updateaccount | --update-account) - _CMD="updateaccount" - ;; - --registeraccount | --register-account) - _CMD="registeraccount" - ;; - --deactivate-account) - _CMD="deactivateaccount" - ;; - --domain | -d) - _dvalue="$2" + --help | -h) + showhelp + return + ;; + --version | -v) + version + return + ;; + --install) + _CMD="install" + ;; + --install-online) + shift + installOnline "$@" + return + ;; + --uninstall) + _CMD="uninstall" + ;; + --upgrade) + _CMD="upgrade" + ;; + --issue) + _CMD="issue" + ;; + --deploy) + _CMD="deploy" + ;; + --sign-csr | --signcsr) + _CMD="signcsr" + ;; + --show-csr | --showcsr) + _CMD="showcsr" + ;; + -i | --install-cert | --installcert) + _CMD="installcert" + ;; + --renew | -r) + _CMD="renew" + ;; + --renew-all | --renewAll | --renewall) + _CMD="renewAll" + ;; + --revoke) + _CMD="revoke" + ;; + --remove) + _CMD="remove" + ;; + --list) + _CMD="list" + ;; + --info) + _CMD="info" + ;; + --install-cronjob | --installcronjob) + _CMD="installcronjob" + ;; + --uninstall-cronjob | --uninstallcronjob) + _CMD="uninstallcronjob" + ;; + --cron) + _CMD="cron" + ;; + --to-pkcs12 | --to-pkcs | --toPkcs) + _CMD="toPkcs" + ;; + --to-pkcs8 | --toPkcs8) + _CMD="toPkcs8" + ;; + --create-account-key | --createAccountKey | --createaccountkey | -cak) + _CMD="createAccountKey" + ;; + --create-domain-key | --createDomainKey | --createdomainkey | -cdk) + _CMD="createDomainKey" + ;; + -ccr | --create-csr | --createCSR | --createcsr) + _CMD="createCSR" + ;; + --deactivate) + _CMD="deactivate" + ;; + --update-account | --updateaccount) + _CMD="updateaccount" + ;; + --register-account | --registeraccount) + _CMD="registeraccount" + ;; + --deactivate-account) + _CMD="deactivateaccount" + ;; + --set-notify) + _CMD="setnotify" + ;; + --set-default-ca) + _CMD="setdefaultca" + ;; + --set-default-chain) + _CMD="setdefaultchain" + ;; + -d | --domain) + _dvalue="$2" - if [ "$_dvalue" ]; then - if _startswith "$_dvalue" "-"; then - _err "'$_dvalue' is not a valid domain for parameter '$1'" - return 1 - fi - if _is_idn "$_dvalue" && ! _exists idn; then - _err "It seems that $_dvalue is an IDN( Internationalized Domain Names), please install 'idn' command first." - return 1 - fi - - if _startswith "$_dvalue" "*."; then - _debug "Wildcard domain" - export ACME_VERSION=2 - fi - if [ -z "$_domain" ]; then - _domain="$_dvalue" - else - if [ "$_altdomains" = "$NO_VALUE" ]; then - _altdomains="$_dvalue" - else - _altdomains="$_altdomains,$_dvalue" - fi - fi - fi - - shift - ;; - - --force | -f) - FORCE="1" - ;; - --staging | --test) - STAGE="1" - ;; - --server) - ACME_DIRECTORY="$2" - _server="$ACME_DIRECTORY" - export ACME_DIRECTORY - shift - ;; - --debug) - if [ -z "$2" ] || _startswith "$2" "-"; then - DEBUG="$DEBUG_LEVEL_DEFAULT" - else - DEBUG="$2" - shift - fi - ;; - --output-insecure) - export OUTPUT_INSECURE=1 - ;; - --webroot | -w) - wvalue="$2" - if [ -z "$_webroot" ]; then - _webroot="$wvalue" - else - _webroot="$_webroot,$wvalue" - fi - shift - ;; - --challenge-alias) - cvalue="$2" - _challenge_alias="$_challenge_alias$cvalue," - shift - ;; - --domain-alias) - cvalue="$DNS_ALIAS_PREFIX$2" - _challenge_alias="$_challenge_alias$cvalue," - shift - ;; - --standalone) - wvalue="$NO_VALUE" - if [ -z "$_webroot" ]; then - _webroot="$wvalue" - else - _webroot="$_webroot,$wvalue" - fi - ;; - --stateless) - wvalue="$MODE_STATELESS" - if [ -z "$_webroot" ]; then - _webroot="$wvalue" - else - _webroot="$_webroot,$wvalue" - fi - ;; - --local-address) - lvalue="$2" - _local_address="$_local_address$lvalue," - shift - ;; - --apache) - wvalue="apache" - if [ -z "$_webroot" ]; then - _webroot="$wvalue" - else - _webroot="$_webroot,$wvalue" - fi - ;; - --nginx) - wvalue="$NGINX" - if [ -z "$_webroot" ]; then - _webroot="$wvalue" - else - _webroot="$_webroot,$wvalue" - fi - ;; - --dns) - wvalue="$W_DNS" - if [ "$2" ] && ! _startswith "$2" "-"; then - wvalue="$2" - shift - fi - if [ -z "$_webroot" ]; then - _webroot="$wvalue" - else - _webroot="$_webroot,$wvalue" - fi - ;; - --dnssleep) - _dnssleep="$2" - Le_DNSSleep="$_dnssleep" - shift - ;; - - --keylength | -k) - _keylength="$2" - shift - ;; - --accountkeylength | -ak) - _accountkeylength="$2" - shift - ;; - - --cert-file | --certpath) - _cert_file="$2" - shift - ;; - --key-file | --keypath) - _key_file="$2" - shift - ;; - --ca-file | --capath) - _ca_file="$2" - shift - ;; - --fullchain-file | --fullchainpath) - _fullchain_file="$2" - shift - ;; - --reloadcmd | --reloadCmd) - _reloadcmd="$2" - shift - ;; - --password) - _password="$2" - shift - ;; - --accountconf) - _accountconf="$2" - ACCOUNT_CONF_PATH="$_accountconf" - shift - ;; - --home) - LE_WORKING_DIR="$2" - shift - ;; - --certhome | --cert-home) - _certhome="$2" - CERT_HOME="$_certhome" - shift - ;; - --config-home) - _confighome="$2" - LE_CONFIG_HOME="$_confighome" - shift - ;; - --useragent) - _useragent="$2" - USER_AGENT="$_useragent" - shift - ;; - --accountemail) - _accountemail="$2" - ACCOUNT_EMAIL="$_accountemail" - shift - ;; - --accountkey) - _accountkey="$2" - ACCOUNT_KEY_PATH="$_accountkey" - shift - ;; - --days) - _days="$2" - Le_RenewalDays="$_days" - shift - ;; - --httpport) - _httpport="$2" - Le_HTTPPort="$_httpport" - shift - ;; - --listraw) - _listraw="raw" - ;; - --stopRenewOnError | --stoprenewonerror | -se) - _stopRenewOnError="1" - ;; - --insecure) - #_insecure="1" - HTTPS_INSECURE="1" - ;; - --ca-bundle) - _ca_bundle="$(_readlink "$2")" - CA_BUNDLE="$_ca_bundle" - shift - ;; - --ca-path) - _ca_path="$2" - CA_PATH="$_ca_path" - shift - ;; - --nocron) - _nocron="1" - ;; - --no-color) - export ACME_NO_COLOR=1 - ;; - --force-color) - export ACME_FORCE_COLOR=1 - ;; - --ecc) - _ecc="isEcc" - ;; - --csr) - _csr="$2" - shift - ;; - --pre-hook) - _pre_hook="$2" - shift - ;; - --post-hook) - _post_hook="$2" - shift - ;; - --renew-hook) - _renew_hook="$2" - shift - ;; - --deploy-hook) - if [ -z "$2" ] || _startswith "$2" "-"; then - _usage "Please specify a value for '--deploy-hook'" + if [ "$_dvalue" ]; then + if _startswith "$_dvalue" "-"; then + _err "'$_dvalue' is not a valid domain for parameter '$1'" return 1 fi - _deploy_hook="$_deploy_hook$2," - shift - ;; - --ocsp-must-staple | --ocsp) - Le_OCSP_Staple="1" - ;; - --always-force-new-domain-key) - if [ -z "$2" ] || _startswith "$2" "-"; then - Le_ForceNewDomainKey=1 + if _is_idn "$_dvalue" && ! _exists idn; then + _err "It seems that $_dvalue is an IDN( Internationalized Domain Names), please install 'idn' command first." + return 1 + fi + + if [ -z "$_domain" ]; then + _domain="$_dvalue" else - Le_ForceNewDomainKey="$2" - shift + if [ "$_altdomains" = "$NO_VALUE" ]; then + _altdomains="$_dvalue" + else + _altdomains="$_altdomains,$_dvalue" + fi fi - ;; - --yes-I-know-dns-manual-mode-enough-go-ahead-please) - export FORCE_DNS_MANUAL=1 - ;; - --log | --logfile) - _log="1" - _logfile="$2" - if _startswith "$_logfile" '-'; then - _logfile="" - else - shift - fi - LOG_FILE="$_logfile" - if [ -z "$LOG_LEVEL" ]; then - LOG_LEVEL="$DEFAULT_LOG_LEVEL" - fi - ;; - --log-level) - _log_level="$2" - LOG_LEVEL="$_log_level" + fi + + shift + ;; + + -f | --force) + FORCE="1" + ;; + --staging | --test) + STAGE="1" + ;; + --server) + _server="$2" + shift + ;; + --debug) + if [ -z "$2" ] || _startswith "$2" "-"; then + DEBUG="$DEBUG_LEVEL_DEFAULT" + else + DEBUG="$2" shift - ;; - --syslog) - if ! _startswith "$2" '-'; then - _syslog="$2" - shift - fi - if [ -z "$_syslog" ]; then - _syslog="$SYSLOG_LEVEL_DEFAULT" - fi - ;; - --auto-upgrade) - _auto_upgrade="$2" - if [ -z "$_auto_upgrade" ] || _startswith "$_auto_upgrade" '-'; then - _auto_upgrade="1" - else - shift - fi - AUTO_UPGRADE="$_auto_upgrade" - ;; - --listen-v4) - _listen_v4="1" - Le_Listen_V4="$_listen_v4" - ;; - --listen-v6) - _listen_v6="1" - Le_Listen_V6="$_listen_v6" - ;; - --openssl-bin) - _openssl_bin="$2" - ACME_OPENSSL_BIN="$_openssl_bin" + fi + ;; + --output-insecure) + export OUTPUT_INSECURE=1 + ;; + -w | --webroot) + wvalue="$2" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + shift + ;; + --challenge-alias) + cvalue="$2" + _challenge_alias="$_challenge_alias$cvalue," + shift + ;; + --domain-alias) + cvalue="$DNS_ALIAS_PREFIX$2" + _challenge_alias="$_challenge_alias$cvalue," + shift + ;; + --standalone) + wvalue="$NO_VALUE" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --alpn) + wvalue="$W_ALPN" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --stateless) + wvalue="$MODE_STATELESS" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --local-address) + lvalue="$2" + _local_address="$_local_address$lvalue," + shift + ;; + --apache) + wvalue="apache" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --nginx) + wvalue="$NGINX" + if [ "$2" ] && ! _startswith "$2" "-"; then + wvalue="$NGINX$2" shift - ;; - --use-wget) - _use_wget="1" - ACME_USE_WGET="1" - ;; - --branch | -b) - export BRANCH="$2" + fi + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --dns) + wvalue="$W_DNS" + if [ "$2" ] && ! _startswith "$2" "-"; then + wvalue="$2" shift - ;; - *) - _err "Unknown parameter : $1" + fi + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; + --dnssleep) + _dnssleep="$2" + Le_DNSSleep="$_dnssleep" + shift + ;; + --keylength | -k) + _keylength="$2" + shift + if [ "$_keylength" ] && ! _isEccKey "$_keylength"; then + export __SELECTED_RSA_KEY=1 + fi + ;; + -ak | --accountkeylength) + _accountkeylength="$2" + shift + ;; + --cert-file | --certpath) + _cert_file="$2" + shift + ;; + --key-file | --keypath) + _key_file="$2" + shift + ;; + --ca-file | --capath) + _ca_file="$2" + shift + ;; + --fullchain-file | --fullchainpath) + _fullchain_file="$2" + shift + ;; + --reloadcmd | --reloadCmd) + _reloadcmd="$2" + shift + ;; + --password) + _password="$2" + shift + ;; + --accountconf) + _accountconf="$2" + ACCOUNT_CONF_PATH="$_accountconf" + shift + ;; + --home) + export LE_WORKING_DIR="$(echo "$2" | sed 's|/$||')" + shift + ;; + --cert-home | --certhome) + _certhome="$2" + export CERT_HOME="$_certhome" + shift + ;; + --config-home) + _confighome="$2" + export LE_CONFIG_HOME="$_confighome" + shift + ;; + --useragent) + _useragent="$2" + USER_AGENT="$_useragent" + shift + ;; + -m | --email | --accountemail) + _accountemail="$2" + export ACCOUNT_EMAIL="$_accountemail" + shift + ;; + --accountkey) + _accountkey="$2" + ACCOUNT_KEY_PATH="$_accountkey" + shift + ;; + --days) + _days="$2" + Le_RenewalDays="$_days" + shift + ;; + --valid-from) + _valid_from="$2" + shift + ;; + --valid-to) + _valid_to="$2" + shift + ;; + --httpport) + _httpport="$2" + Le_HTTPPort="$_httpport" + shift + ;; + --tlsport) + _tlsport="$2" + Le_TLSPort="$_tlsport" + shift + ;; + --listraw) + _listraw="raw" + ;; + -se | --stop-renew-on-error | --stopRenewOnError | --stoprenewonerror) + _stopRenewOnError="1" + ;; + --insecure) + #_insecure="1" + HTTPS_INSECURE="1" + ;; + --ca-bundle) + _ca_bundle="$(_readlink "$2")" + CA_BUNDLE="$_ca_bundle" + shift + ;; + --ca-path) + _ca_path="$2" + CA_PATH="$_ca_path" + shift + ;; + --no-cron | --nocron) + _nocron="1" + ;; + --no-profile | --noprofile) + _noprofile="1" + ;; + --no-color) + export ACME_NO_COLOR=1 + ;; + --force-color) + export ACME_FORCE_COLOR=1 + ;; + --ecc) + _ecc="isEcc" + ;; + --csr) + _csr="$2" + shift + ;; + --pre-hook) + _pre_hook="$2" + shift + ;; + --post-hook) + _post_hook="$2" + shift + ;; + --renew-hook) + _renew_hook="$2" + shift + ;; + --deploy-hook) + if [ -z "$2" ] || _startswith "$2" "-"; then + _usage "Please specify a value for '--deploy-hook'" return 1 - ;; + fi + _deploy_hook="$_deploy_hook$2," + shift + ;; + --ocsp-must-staple | --ocsp) + Le_OCSP_Staple="1" + ;; + --always-force-new-domain-key) + if [ -z "$2" ] || _startswith "$2" "-"; then + Le_ForceNewDomainKey=1 + else + Le_ForceNewDomainKey="$2" + shift + fi + ;; + --yes-I-know-dns-manual-mode-enough-go-ahead-please) + export FORCE_DNS_MANUAL=1 + ;; + --log | --logfile) + _log="1" + _logfile="$2" + if _startswith "$_logfile" '-'; then + _logfile="" + else + shift + fi + LOG_FILE="$_logfile" + if [ -z "$LOG_LEVEL" ]; then + LOG_LEVEL="$DEFAULT_LOG_LEVEL" + fi + ;; + --log-level) + _log_level="$2" + LOG_LEVEL="$_log_level" + shift + ;; + --syslog) + if ! _startswith "$2" '-'; then + _syslog="$2" + shift + fi + if [ -z "$_syslog" ]; then + _syslog="$SYSLOG_LEVEL_DEFAULT" + fi + ;; + --auto-upgrade) + _auto_upgrade="$2" + if [ -z "$_auto_upgrade" ] || _startswith "$_auto_upgrade" '-'; then + _auto_upgrade="1" + else + shift + fi + AUTO_UPGRADE="$_auto_upgrade" + ;; + --listen-v4) + _listen_v4="1" + Le_Listen_V4="$_listen_v4" + ;; + --listen-v6) + _listen_v6="1" + Le_Listen_V6="$_listen_v6" + ;; + --openssl-bin) + _openssl_bin="$2" + ACME_OPENSSL_BIN="$_openssl_bin" + shift + ;; + --use-wget) + _use_wget="1" + ACME_USE_WGET="1" + ;; + --branch | -b) + export BRANCH="$2" + shift + ;; + --notify-hook) + _nhook="$2" + if _startswith "$_nhook" "-"; then + _err "'$_nhook' is not a hook name for '$1'" + return 1 + fi + if [ "$_notify_hook" ]; then + _notify_hook="$_notify_hook,$_nhook" + else + _notify_hook="$_nhook" + fi + shift + ;; + --notify-level) + _nlevel="$2" + if _startswith "$_nlevel" "-"; then + _err "'$_nlevel' is not a integer for '$1'" + return 1 + fi + _notify_level="$_nlevel" + shift + ;; + --notify-mode) + _nmode="$2" + if _startswith "$_nmode" "-"; then + _err "'$_nmode' is not a integer for '$1'" + return 1 + fi + _notify_mode="$_nmode" + shift + ;; + --notify-source) + _nsource="$2" + if _startswith "$_nsource" "-"; then + _err "'$_nsource' is not valid host name for '$1'" + return 1 + fi + _notify_source="$_nsource" + shift + ;; + --revoke-reason) + _revoke_reason="$2" + if _startswith "$_revoke_reason" "-"; then + _err "'$_revoke_reason' is not a integer for '$1'" + return 1 + fi + shift + ;; + --eab-kid) + _eab_kid="$2" + shift + ;; + --eab-hmac-key) + _eab_hmac_key="$2" + shift + ;; + --preferred-chain) + _preferred_chain="$2" + shift + ;; + *) + _err "Unknown parameter : $1" + return 1 + ;; esac shift 1 done + if [ "$_server" ]; then + _selectServer "$_server" "${_ecc:-$_keylength}" + _server="$ACME_DIRECTORY" + fi + if [ "${_CMD}" != "install" ]; then + if [ "$__INTERACTIVE" ] && ! _checkSudo; then + if [ -z "$FORCE" ]; then + #Use "echo" here, instead of _info. it's too early + echo "It seems that you are using sudo, please read this link first:" + echo "$_SUDO_WIKI" + return 1 + fi + fi __initHome if [ "$_log" ]; then if [ -z "$_logfile" ]; then @@ -6122,79 +7847,90 @@ _process() { _debug "Using server: $_server" fi fi - + _debug "Running cmd: ${_CMD}" case "${_CMD}" in - install) install "$_nocron" "$_confighome" ;; - uninstall) uninstall "$_nocron" ;; - upgrade) upgrade ;; - issue) - issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" - ;; - deploy) - deploy "$_domain" "$_deploy_hook" "$_ecc" - ;; - signcsr) - signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" - ;; - showcsr) - showcsr "$_csr" "$_domain" - ;; - installcert) - installcert "$_domain" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_ecc" - ;; - renew) - renew "$_domain" "$_ecc" - ;; - renewAll) - renewAll "$_stopRenewOnError" - ;; - revoke) - revoke "$_domain" "$_ecc" - ;; - remove) - remove "$_domain" "$_ecc" - ;; - deactivate) - deactivate "$_domain,$_altdomains" - ;; - registeraccount) - registeraccount "$_accountkeylength" - ;; - updateaccount) - updateaccount - ;; - deactivateaccount) - deactivateaccount - ;; - list) - list "$_listraw" - ;; - installcronjob) installcronjob "$_confighome" ;; - uninstallcronjob) uninstallcronjob ;; - cron) cron ;; - toPkcs) - toPkcs "$_domain" "$_password" "$_ecc" - ;; - toPkcs8) - toPkcs8 "$_domain" "$_ecc" - ;; - createAccountKey) - createAccountKey "$_accountkeylength" - ;; - createDomainKey) - createDomainKey "$_domain" "$_keylength" - ;; - createCSR) - createCSR "$_domain" "$_altdomains" "$_ecc" - ;; - - *) - if [ "$_CMD" ]; then - _err "Invalid command: $_CMD" - fi - showhelp - return 1 - ;; + install) install "$_nocron" "$_confighome" "$_noprofile" "$_accountemail" ;; + uninstall) uninstall "$_nocron" ;; + upgrade) upgrade ;; + issue) + issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" + ;; + deploy) + deploy "$_domain" "$_deploy_hook" "$_ecc" + ;; + signcsr) + signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" + ;; + showcsr) + showcsr "$_csr" "$_domain" + ;; + installcert) + installcert "$_domain" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_ecc" + ;; + renew) + renew "$_domain" "$_ecc" "$_server" + ;; + renewAll) + renewAll "$_stopRenewOnError" "$_server" + ;; + revoke) + revoke "$_domain" "$_ecc" "$_revoke_reason" + ;; + remove) + remove "$_domain" "$_ecc" + ;; + deactivate) + deactivate "$_domain,$_altdomains" + ;; + registeraccount) + registeraccount "$_accountkeylength" "$_eab_kid" "$_eab_hmac_key" + ;; + updateaccount) + updateaccount + ;; + deactivateaccount) + deactivateaccount + ;; + list) + list "$_listraw" "$_domain" + ;; + info) + info "$_domain" "$_ecc" + ;; + installcronjob) installcronjob "$_confighome" ;; + uninstallcronjob) uninstallcronjob ;; + cron) cron ;; + toPkcs) + toPkcs "$_domain" "$_password" "$_ecc" + ;; + toPkcs8) + toPkcs8 "$_domain" "$_ecc" + ;; + createAccountKey) + createAccountKey "$_accountkeylength" + ;; + createDomainKey) + createDomainKey "$_domain" "$_keylength" + ;; + createCSR) + createCSR "$_domain" "$_altdomains" "$_ecc" + ;; + setnotify) + setnotify "$_notify_hook" "$_notify_level" "$_notify_mode" "$_notify_source" + ;; + setdefaultca) + setdefaultca + ;; + setdefaultchain) + setdefaultchain "$_preferred_chain" + ;; + *) + if [ "$_CMD" ]; then + _err "Invalid command: $_CMD" + fi + showhelp + return 1 + ;; esac _ret="$?" if [ "$_ret" != "0" ]; then @@ -6232,12 +7968,6 @@ _process() { } -if [ "$INSTALLONLINE" ]; then - INSTALLONLINE="" - _installOnline - exit -fi - main() { [ -z "$1" ] && showhelp && return if _startswith "$1" '-'; then _process "$@"; else "$@"; fi diff --git a/deploy/README.md b/deploy/README.md index 3811a5b5..6f51d38c 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,337 +1,5 @@ # Using deploy api -Before you can deploy your cert, you must [issue the cert first](https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert). +deploy hook usage: -Here are the scripts to deploy the certs/key to the server/services. - -## 1. Deploy the certs to your cpanel host - -If you want to deploy using cpanel UAPI see 7. - -(cpanel deploy hook is not finished yet, this is just an example.) - - - -Then you can deploy now: - -```sh -export DEPLOY_CPANEL_USER=myusername -export DEPLOY_CPANEL_PASSWORD=PASSWORD -acme.sh --deploy -d example.com --deploy-hook cpanel -``` - -## 2. Deploy ssl cert on kong proxy engine based on api - -Before you can deploy your cert, you must [issue the cert first](https://github.com/Neilpang/acme.sh/wiki/How-to-issue-a-cert). -Currently supports Kong-v0.10.x. - -```sh -acme.sh --deploy -d ftp.example.com --deploy-hook kong -``` - -## 3. Deploy the cert to remote server through SSH access - -The ssh deploy plugin allows you to deploy certificates to a remote host -using SSH command to connect to the remote server. The ssh plugin is invoked -with the following command... - -```sh -acme.sh --deploy -d example.com --deploy-hook ssh -``` -Prior to running this for the first time you must tell the plugin where -and how to deploy the certificates. This is done by exporting the following -environment variables. This is not required for subsequent runs as the -values are stored by acme.sh in the domain configuration files. - -Required... -``` -export DEPLOY_SSH_USER=username -``` -Optional... -``` -export DEPLOY_SSH_CMD=custom ssh command -export DEPLOY_SSH_SERVER=url or ip address of remote host -export DEPLOY_SSH_KEYFILE=filename for private key -export DEPLOY_SSH_CERTFILE=filename for certificate file -export DEPLOY_SSH_CAFILE=filename for intermediate CA file -export DEPLOY_SSH_FULLCHAIN=filename for fullchain file -export DEPLOY_SSH_REMOTE_CMD=command to execute on remote host -export DEPLOY_SSH_BACKUP=yes or no -``` - -**DEPLOY_SSH_USER** -Username at the remote host that SSH will login with. Note that -SSH must be able to login to remote host without a password... SSH Keys -must have been exchanged with the remote host. Validate and test that you -can login to USER@URL from the host running acme.sh before using this script. - -The USER@URL at the remote server must also have has permissions to write to -the target location of the certificate files and to execute any commands -(e.g. to stop/start services). - -**DEPLOY_SSH_CMD** -You can customize the ssh command used to connect to the remote host. For example -if you need to connect to a specific port at the remote server you can set this -to, for example, "ssh -p 22" or to use `sshpass` to provide password inline -instead of exchanging ssh keys (this is not recommended, using keys is -more secure). - -**DEPLOY_SSH_SERVER** -URL or IP Address of the remote server. If not provided then the domain -name provided on the acme.sh --deploy command line is used. - -**DEPLOY_SSH_KEYFILE** -Target filename for the private key issued by LetsEncrypt. - -**DEPLOY_SSH_CERTFILE** -Target filename for the certificate issued by LetsEncrypt. -If this is the same as the previous filename (for keyfile) then it is -appended to the same file. - -**DEPLOY_SSH_CAFILE** -Target filename for the CA intermediate certificate issued by LetsEncrypt. -If this is the same as a previous filename (for keyfile or certfile) then -it is appended to the same file. - -**DEPLOY_SSH_FULLCHAIN** -Target filename for the fullchain certificate issued by LetsEncrypt. -If this is the same as a previous filename (for keyfile, certfile or -cafile) then it is appended to the same file. - -**DEPLOY_SSH_REMOTE_CMD** -Command to execute on the remote server after copying any certificates. This -could be any additional command required for example to stop and restart -the service. - -**DEPLOY_SSH_BACKUP** -Before writing a certificate file to the remote server the existing -certificate will be copied to a backup directory on the remote server. -These are placed in a hidden directory in the home directory of the SSH -user -```sh -~/.acme_ssh_deploy/[domain name]-backup-[timestamp] -``` -Any backups older than 180 days will be deleted when new certificates -are deployed. This defaults to "yes" set to "no" to disable backup. - -###Examples using SSH deploy -The following example illustrates deploying certificates to a QNAP NAS -(tested with QTS version 4.2.3) - -```sh -export DEPLOY_SSH_USER="admin" -export DEPLOY_SSH_KEYFILE="/etc/stunnel/stunnel.pem" -export DEPLOY_SSH_CERTFILE="/etc/stunnel/stunnel.pem" -export DEPLOY_SSH_CAFILE="/etc/stunnel/uca.pem" -export DEPLOY_SSH_REMOTE_CMD="/etc/init.d/stunnel.sh restart" - -acme.sh --deploy -d qnap.example.com --deploy-hook ssh -``` -Note how in this example both the private key and certificate point to -the same file. This will result in the certificate being appended -to the same file as the private key... a common requirement of several -services. - -The next example illustrates deploying certificates to a Unifi -Controller (tested with version 5.4.11). - -```sh -export DEPLOY_SSH_USER="root" -export DEPLOY_SSH_KEYFILE="/var/lib/unifi/unifi.example.com.key" -export DEPLOY_SSH_FULLCHAIN="/var/lib/unifi/unifi.example.com.cer" -export DEPLOY_SSH_REMOTE_CMD="openssl pkcs12 -export \ - -inkey /var/lib/unifi/unifi.example.com.key \ - -in /var/lib/unifi/unifi.example.com.cer \ - -out /var/lib/unifi/unifi.example.com.p12 \ - -name ubnt -password pass:temppass \ - && keytool -importkeystore -deststorepass aircontrolenterprise \ - -destkeypass aircontrolenterprise \ - -destkeystore /var/lib/unifi/keystore \ - -srckeystore /var/lib/unifi/unifi.example.com.p12 \ - -srcstoretype PKCS12 -srcstorepass temppass -alias ubnt -noprompt \ - && service unifi restart" - -acme.sh --deploy -d unifi.example.com --deploy-hook ssh -``` -In this example we execute several commands on the remote host -after the certificate files have been copied... to generate a pkcs12 file -compatible with Unifi, to import it into the Unifi keystore and then finally -to restart the service. - -Note also that once the certificate is imported -into the keystore the individual certificate files are no longer -required. We could if we desired delete those files immediately. If we -do that then we should disable backup at the remote host (as there are -no files to backup -- they were erased during deployment). For example... -```sh -export DEPLOY_SSH_BACKUP=no -# modify the end of the remote command... -&& rm /var/lib/unifi/unifi.example.com.key \ - /var/lib/unifi/unifi.example.com.cer \ - /var/lib/unifi/unifi.example.com.p12 \ -&& service unifi restart -``` - -## 4. Deploy the cert to local vsftpd server - -```sh -acme.sh --deploy -d ftp.example.com --deploy-hook vsftpd -``` - -The default vsftpd conf file is `/etc/vsftpd.conf`, if your vsftpd conf is not in the default location, you can specify one: - -```sh -export DEPLOY_VSFTPD_CONF="/etc/vsftpd.conf" - -acme.sh --deploy -d ftp.example.com --deploy-hook vsftpd -``` - -The default command to restart vsftpd server is `service vsftpd restart`, if it doesn't work, you can specify one: - -```sh -export DEPLOY_VSFTPD_RELOAD="/etc/init.d/vsftpd restart" - -acme.sh --deploy -d ftp.example.com --deploy-hook vsftpd -``` - -## 5. Deploy the cert to local exim4 server - -```sh -acme.sh --deploy -d ftp.example.com --deploy-hook exim4 -``` - -The default exim4 conf file is `/etc/exim/exim.conf`, if your exim4 conf is not in the default location, you can specify one: - -```sh -export DEPLOY_EXIM4_CONF="/etc/exim4/exim4.conf.template" - -acme.sh --deploy -d ftp.example.com --deploy-hook exim4 -``` - -The default command to restart exim4 server is `service exim4 restart`, if it doesn't work, you can specify one: - -```sh -export DEPLOY_EXIM4_RELOAD="/etc/init.d/exim4 restart" - -acme.sh --deploy -d ftp.example.com --deploy-hook exim4 -``` - -## 6. Deploy the cert to OSX Keychain - -```sh -acme.sh --deploy -d ftp.example.com --deploy-hook keychain -``` - -## 7. Deploy to cpanel host using UAPI - -This hook is using UAPI and works in cPanel & WHM version 56 or newer. -``` -acme.sh --deploy -d example.com --deploy-hook cpanel_uapi -``` -DEPLOY_CPANEL_USER is required only if you run the script as root and it should contain cpanel username. -```sh -export DEPLOY_CPANEL_USER=username -acme.sh --deploy -d example.com --deploy-hook cpanel_uapi -``` -Please note, that the cpanel_uapi hook will deploy only the first domain when your certificate will automatically renew. Therefore you should issue a separate certificate for each domain. - -## 8. Deploy the cert to your FRITZ!Box router - -You must specify the credentials that have administrative privileges on the FRITZ!Box in order to deploy the certificate, plus the URL of your FRITZ!Box, through the following environment variables: -```sh -$ export DEPLOY_FRITZBOX_USERNAME=my_username -$ export DEPLOY_FRITZBOX_PASSWORD=the_password -$ export DEPLOY_FRITZBOX_URL=https://fritzbox.example.com -``` - -After the first deployment, these values will be stored in your $HOME/.acme.sh/account.conf. You may now deploy the certificate like this: - -```sh -acme.sh --deploy -d fritzbox.example.com --deploy-hook fritzbox -``` - -## 9. Deploy the cert to strongswan - -```sh -acme.sh --deploy -d ftp.example.com --deploy-hook strongswan -``` - -## 10. Deploy the cert to HAProxy - -You must specify the path where you want the concatenated key and certificate chain written. -```sh -export DEPLOY_HAPROXY_PEM_PATH=/etc/haproxy -``` - -You may optionally define the command to reload HAProxy. The value shown below will be used as the default if you don't set this environment variable. - -```sh -export DEPLOY_HAPROXY_RELOAD="/usr/sbin/service haproxy restart" -``` - -You can then deploy the certificate as follows -```sh -acme.sh --deploy -d haproxy.example.com --deploy-hook haproxy -``` - -The path for the PEM file will be stored with the domain configuration and will be available when renewing, so that deploy will happen automatically when renewed. - -## 11. Deploy your cert to Gitlab pages - -You must define the API key and the informations for the project and Gitlab page you are updating the certificate for. - -```sh -# The token can be created in your user settings under "Access Tokens" -export GITLAB_TOKEN="xxxxxxxxxxx" - -# The project ID is displayed on the home page of the project -export GITLAB_PROJECT_ID=12345678 - -# The domain must match the one defined for the Gitlab page, without "https://" -export GITLAB_DOMAIN="www.mydomain.com" -``` - -You can then deploy the certificate as follows - -```sh -acme.sh --deploy -d www.mydomain.com --deploy-hook gitlab -``` - -## 12. Deploy the cert to your AliCDN account - -You must specify the aliyun account credentials in order to deploy the certificate, optionally specify the prefix of your domain if you use wildcard in cdn domain, through the following environment variables: -```sh -export DEPLOY_CDN_Ali_Key="AK" -export DEPLOY_CDN_Ali_Secret="SK" -export DEPLOY_CDN_Ali_Prefix="" -``` - -After the first deployment, these values will be stored in your domain deploy conf. You may now deploy the certificate like this: - -```sh -acme.sh --deploy -d example.com --deploy-hook cdn_ali -``` - -If your cdn domain is cdn.example.com and cert domain is also cdn.example.com, you can leave the prefix empty in deployment: -```sh -export DEPLOY_CDN_Ali_Key="AK" -export DEPLOY_CDN_Ali_Secret="SK" -export DEPLOY_CDN_Ali_Prefix="" -acme.sh --deploy -d cdn.example.com --deploy-hook cdn_ali -``` - -If your cdn domain is cdn.example.com but cert domain is example.com(subject alternative names: example.com,\*.example.com), you must specify the prefix in deployment: -```sh -export DEPLOY_CDN_Ali_Key="AK" -export DEPLOY_CDN_Ali_Secret="SK" -export DEPLOY_CDN_Ali_Prefix="cdn." -acme.sh --deploy -d example.com --deploy-hook cdn_ali -``` - -If your cdn domain is \*.example.com (added wildcard domain in AliCDN console) but cert domain is example.com(subject alternative names: example.com,\*.example.com), you must specify the prefix only a dot(.) in deployment: -```sh -export DEPLOY_CDN_Ali_Key="AK" -export DEPLOY_CDN_Ali_Secret="SK" -export DEPLOY_CDN_Ali_Prefix="." -acme.sh --deploy -d example.com --deploy-hook cdn_ali -``` +https://github.com/acmesh-official/acme.sh/wiki/deployhooks \ No newline at end of file diff --git a/deploy/cleverreach.sh b/deploy/cleverreach.sh new file mode 100644 index 00000000..a460a139 --- /dev/null +++ b/deploy/cleverreach.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env sh +# Here is the script to deploy the cert to your CleverReach Account using the CleverReach REST API. +# Your OAuth needs the right scope, please contact CleverReach support for that. +# +# Written by Jan-Philipp Benecke +# Public domain, 2020 +# +# Following environment variables must be set: +# +#export DEPLOY_CLEVERREACH_CLIENT_ID=myid +#export DEPLOY_CLEVERREACH_CLIENT_SECRET=mysecret + +cleverreach_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + + _rest_endpoint="https://rest.cleverreach.com" + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + _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." + return 1 + fi + if [ -z "${DEPLOY_CLEVERREACH_CLIENT_SECRET}" ]; then + _err "CleverReach client secret is not found, please define DEPLOY_CLEVERREACH_CLIENT_SECRET." + return 1 + fi + + _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" "$_rest_endpoint/oauth/token.php" "" "POST" "application/json")" + + _debug _data "$_data" + _debug _auth_result "$_auth_result" + + _regex=".*\"access_token\":\"\([-._0-9A-Za-z]*\)\".*$" + _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" "$_rest_endpoint/v3/ssl" "" "POST" "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" + return 0 + else + _debug _add_cert_result "$_add_cert_result" + _err "Unable to update certificate" + return 1 + fi +} 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/cpanel_uapi.sh b/deploy/cpanel_uapi.sh index 44844f79..e5381b61 100644 --- a/deploy/cpanel_uapi.sh +++ b/deploy/cpanel_uapi.sh @@ -3,18 +3,29 @@ # Uses command line uapi. --user option is needed only if run as root. # Returns 0 when success. # +# Configure DEPLOY_CPANEL_AUTO_<...> options to enable or restrict automatic +# detection of deployment targets through UAPI (if not set, defaults below are used.) +# - ENABLED : 'true' for multi-site / wildcard capability; otherwise single-site mode. +# - NOMATCH : 'true' to allow deployment to sites that do not match the certificate. +# - INCLUDE : Comma-separated list - sites must match this field. +# - EXCLUDE : Comma-separated list - sites must NOT match this field. +# INCLUDE/EXCLUDE both support non-lexical, glob-style matches using '*' +# # Please note that I am no longer using Github. If you want to report an issue # or contact me, visit https://forum.webseodesigners.com/web-design-seo-and-hosting-f16/ # # Written by Santeri Kannisto # Public domain, 2017-2018 - -#export DEPLOY_CPANEL_USER=myusername +# +# export DEPLOY_CPANEL_USER=myusername +# export DEPLOY_CPANEL_AUTO_ENABLED='true' +# export DEPLOY_CPANEL_AUTO_NOMATCH='false' +# export DEPLOY_CPANEL_AUTO_INCLUDE='*' +# export DEPLOY_CPANEL_AUTO_EXCLUDE='' ######## Public functions ##################### #domain keyfile certfile cafile fullchain - cpanel_uapi_deploy() { _cdomain="$1" _ckey="$2" @@ -22,6 +33,9 @@ cpanel_uapi_deploy() { _cca="$4" _cfullchain="$5" + # re-declare vars inherited from acme.sh but not passed to make ShellCheck happy + : "${Le_Alt:=""}" + _debug _cdomain "$_cdomain" _debug _ckey "$_ckey" _debug _ccert "$_ccert" @@ -32,31 +46,166 @@ cpanel_uapi_deploy() { _err "The command uapi is not found." return 1 fi + + # declare useful constants + uapi_error_response='status: 0' + # read cert and key files and urlencode both _cert=$(_url_encode <"$_ccert") _key=$(_url_encode <"$_ckey") - _debug _cert "$_cert" - _debug _key "$_key" + _debug2 _cert "$_cert" + _debug2 _key "$_key" if [ "$(id -u)" = 0 ]; then - if [ -z "$DEPLOY_CPANEL_USER" ]; then + _getdeployconf DEPLOY_CPANEL_USER + # fallback to _readdomainconf for old installs + if [ -z "${DEPLOY_CPANEL_USER:=$(_readdomainconf DEPLOY_CPANEL_USER)}" ]; then _err "It seems that you are root, please define the target user name: export DEPLOY_CPANEL_USER=username" return 1 fi - _savedomainconf DEPLOY_CPANEL_USER "$DEPLOY_CPANEL_USER" - _response=$(uapi --user="$DEPLOY_CPANEL_USER" SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key") - else - _response=$(uapi SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key") - fi - error_response="status: 0" - if test "${_response#*$error_response}" != "$_response"; then - _err "Error in deploying certificate:" - _err "$_response" - return 1 + _debug DEPLOY_CPANEL_USER "$DEPLOY_CPANEL_USER" + _savedeployconf DEPLOY_CPANEL_USER "$DEPLOY_CPANEL_USER" + + _uapi_user="$DEPLOY_CPANEL_USER" fi - _debug response "$_response" - _info "Certificate successfully deployed" - return 0 + # Load all AUTO envars and set defaults - see above for usage + __cpanel_initautoparam ENABLED 'true' + __cpanel_initautoparam NOMATCH 'false' + __cpanel_initautoparam INCLUDE '*' + __cpanel_initautoparam EXCLUDE '' + + # Auto mode + if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then + # call API for site config + _response=$(uapi DomainInfo list_domains) + # exit if error in response + if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then + _err "Error in deploying certificate - cannot retrieve sitelist:" + _err "\n$_response" + return 1 + fi + + # parse response to create site list + sitelist=$(__cpanel_parse_response "$_response") + _debug "UAPI sites found: $sitelist" + + # filter sitelist using configured domains + # skip if NOMATCH is "true" + if [ "$DEPLOY_CPANEL_AUTO_NOMATCH" = "true" ]; then + _debug "DEPLOY_CPANEL_AUTO_NOMATCH is true" + _info "UAPI nomatch mode is enabled - Will not validate sites are valid for the certificate" + else + _debug "DEPLOY_CPANEL_AUTO_NOMATCH is false" + d="$(echo "${Le_Alt}," | sed -e "s/^$_cdomain,//" -e "s/,$_cdomain,/,/")" + d="$(echo "$_cdomain,$d" | tr ',' '\n' | sed -e 's/\./\\./g' -e 's/\*/\[\^\.\]\*/g')" + sitelist="$(echo "$sitelist" | grep -ix "$d")" + _debug2 "Matched UAPI sites: $sitelist" + fi + + # filter sites that do not match $DEPLOY_CPANEL_AUTO_INCLUDE + _info "Applying sitelist filter DEPLOY_CPANEL_AUTO_INCLUDE: $DEPLOY_CPANEL_AUTO_INCLUDE" + sitelist="$(echo "$sitelist" | grep -ix "$(echo "$DEPLOY_CPANEL_AUTO_INCLUDE" | tr ',' '\n' | sed -e 's/\./\\./g' -e 's/\*/\.\*/g')")" + _debug2 "Remaining sites: $sitelist" + + # filter sites that match $DEPLOY_CPANEL_AUTO_EXCLUDE + _info "Applying sitelist filter DEPLOY_CPANEL_AUTO_EXCLUDE: $DEPLOY_CPANEL_AUTO_EXCLUDE" + sitelist="$(echo "$sitelist" | grep -vix "$(echo "$DEPLOY_CPANEL_AUTO_EXCLUDE" | tr ',' '\n' | sed -e 's/\./\\./g' -e 's/\*/\.\*/g')")" + _debug2 "Remaining sites: $sitelist" + + # counter for success / failure check + successes=0 + if [ -n "$sitelist" ]; then + sitetotal="$(echo "$sitelist" | wc -l)" + _debug "$sitetotal sites to deploy" + else + sitetotal=0 + _debug "No sites to deploy" + fi + + # for each site: call uapi to publish cert and log result. Only return failure if all fail + for site in $sitelist; do + # call uapi to publish cert, check response for errors and log them. + if [ -n "$_uapi_user" ]; then + _response=$(uapi --user="$_uapi_user" SSL install_ssl domain="$site" cert="$_cert" key="$_key") + else + _response=$(uapi SSL install_ssl domain="$site" cert="$_cert" key="$_key") + fi + if [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then + _err "Error in deploying certificate to $site:" + _err "$_response" + else + successes=$((successes + 1)) + _debug "$_response" + _info "Succcessfully deployed to $site" + fi + done + + # Raise error if all updates fail + if [ "$sitetotal" -gt 0 ] && [ "$successes" -eq 0 ]; then + _err "Could not deploy to any of $sitetotal sites via UAPI" + _debug "successes: $successes, sitetotal: $sitetotal" + return 1 + fi + + _info "Successfully deployed certificate to $successes of $sitetotal sites via UAPI" + return 0 + else + # "classic" mode - will only try to deploy to the primary domain; will not check UAPI first + if [ -n "$_uapi_user" ]; then + _response=$(uapi --user="$_uapi_user" SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key") + else + _response=$(uapi SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key") + fi + + if [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then + _err "Error in deploying certificate:" + _err "$_response" + return 1 + fi + + _debug response "$_response" + _info "Certificate successfully deployed" + return 0 + fi +} + +######## Private functions ##################### + +# Internal utility to process YML from UAPI - looks at main_domain, sub_domains, addon domains and parked domains +#[response] +__cpanel_parse_response() { + if [ $# -gt 0 ]; then resp="$*"; else resp="$(cat)"; fi + + echo "$resp" | + sed -En \ + -e 's/\r$//' \ + -e 's/^( *)([_.[:alnum:]]+) *: *(.*)/\1,\2,\3/p' \ + -e 's/^( *)- (.*)/\1,-,\2/p' | + awk -F, '{ + level = length($1)/2; + section[level] = $2; + for (i in section) {if (i > level) {delete section[i]}} + if (length($3) > 0) { + prefix=""; + for (i=0; i < level; i++) + { prefix = (prefix)(section[i])("/") } + printf("%s%s=%s\n", prefix, $2, $3); + } + }' | + sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p' +} + +# Load parameter by prefix+name - fallback to default if not set, and save to config +#pname pdefault +__cpanel_initautoparam() { + pname="$1" + pdefault="$2" + pkey="DEPLOY_CPANEL_AUTO_$pname" + + _getdeployconf "$pkey" + [ -n "$(eval echo "\"\$$pkey\"")" ] || eval "$pkey=\"$pdefault\"" + _debug2 "$pkey" "$(eval echo "\"\$$pkey\"")" + _savedeployconf "$pkey" "$(eval echo "\"\$$pkey\"")" } diff --git a/deploy/docker.sh b/deploy/docker.sh new file mode 100755 index 00000000..c9815d5b --- /dev/null +++ b/deploy/docker.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env sh + +#DEPLOY_DOCKER_CONTAINER_LABEL="xxxxxxx" + +#DEPLOY_DOCKER_CONTAINER_KEY_FILE="/path/to/key.pem" +#DEPLOY_DOCKER_CONTAINER_CERT_FILE="/path/to/cert.pem" +#DEPLOY_DOCKER_CONTAINER_CA_FILE="/path/to/ca.pem" +#DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/path/to/fullchain.pem" +#DEPLOY_DOCKER_CONTAINER_RELOAD_CMD="service nginx force-reload" + +_DEPLOY_DOCKER_WIKI="https://github.com/acmesh-official/acme.sh/wiki/deploy-to-docker-containers" + +_DOCKER_HOST_DEFAULT="/var/run/docker.sock" + +docker_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + _debug _cdomain "$_cdomain" + _getdeployconf DEPLOY_DOCKER_CONTAINER_LABEL + _debug2 DEPLOY_DOCKER_CONTAINER_LABEL "$DEPLOY_DOCKER_CONTAINER_LABEL" + if [ -z "$DEPLOY_DOCKER_CONTAINER_LABEL" ]; then + _err "The DEPLOY_DOCKER_CONTAINER_LABEL variable is not defined, we use this label to find the container." + _err "See: $_DEPLOY_DOCKER_WIKI" + fi + + _savedeployconf DEPLOY_DOCKER_CONTAINER_LABEL "$DEPLOY_DOCKER_CONTAINER_LABEL" + + if [ "$DOCKER_HOST" ]; then + _saveaccountconf DOCKER_HOST "$DOCKER_HOST" + fi + + if _exists docker && docker version | grep -i docker >/dev/null; then + _info "Using docker command" + export _USE_DOCKER_COMMAND=1 + else + export _USE_DOCKER_COMMAND= + fi + + export _USE_UNIX_SOCKET= + if [ -z "$_USE_DOCKER_COMMAND" ]; then + export _USE_REST= + if [ "$DOCKER_HOST" ]; then + _debug "Try use docker host: $DOCKER_HOST" + export _USE_REST=1 + else + export _DOCKER_SOCK="$_DOCKER_HOST_DEFAULT" + _debug "Try use $_DOCKER_SOCK" + if [ ! -e "$_DOCKER_SOCK" ] || [ ! -w "$_DOCKER_SOCK" ]; then + _err "$_DOCKER_SOCK is not available" + return 1 + fi + export _USE_UNIX_SOCKET=1 + if ! _exists "curl"; then + _err "Please install curl first." + _err "We need curl to work." + return 1 + fi + if ! _check_curl_version; then + return 1 + fi + fi + fi + + _getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_FILE + _debug2 DEPLOY_DOCKER_CONTAINER_KEY_FILE "$DEPLOY_DOCKER_CONTAINER_KEY_FILE" + if [ "$DEPLOY_DOCKER_CONTAINER_KEY_FILE" ]; then + _savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_FILE "$DEPLOY_DOCKER_CONTAINER_KEY_FILE" + fi + + _getdeployconf DEPLOY_DOCKER_CONTAINER_CERT_FILE + _debug2 DEPLOY_DOCKER_CONTAINER_CERT_FILE "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" + if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then + _savedeployconf DEPLOY_DOCKER_CONTAINER_CERT_FILE "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" + fi + + _getdeployconf DEPLOY_DOCKER_CONTAINER_CA_FILE + _debug2 DEPLOY_DOCKER_CONTAINER_CA_FILE "$DEPLOY_DOCKER_CONTAINER_CA_FILE" + if [ "$DEPLOY_DOCKER_CONTAINER_CA_FILE" ]; then + _savedeployconf DEPLOY_DOCKER_CONTAINER_CA_FILE "$DEPLOY_DOCKER_CONTAINER_CA_FILE" + fi + + _getdeployconf DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE + _debug2 DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE" + if [ "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE" ]; then + _savedeployconf DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE" + fi + + _getdeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD + _debug2 DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" + if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then + _savedeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" "base64" + fi + + _cid="$(_get_id "$DEPLOY_DOCKER_CONTAINER_LABEL")" + _info "Container id: $_cid" + if [ -z "$_cid" ]; then + _err "can not find container id" + return 1 + fi + + if [ "$DEPLOY_DOCKER_CONTAINER_KEY_FILE" ]; then + if ! _docker_cp "$_cid" "$_ckey" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then + return 1 + fi + fi + + if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then + if ! _docker_cp "$_cid" "$_ccert" "$DEPLOY_DOCKER_CONTAINER_CERT_FILE"; then + return 1 + fi + fi + + if [ "$DEPLOY_DOCKER_CONTAINER_CA_FILE" ]; then + if ! _docker_cp "$_cid" "$_cca" "$DEPLOY_DOCKER_CONTAINER_CA_FILE"; then + return 1 + fi + fi + + if [ "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE" ]; then + if ! _docker_cp "$_cid" "$_cfullchain" "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE"; then + return 1 + fi + fi + + if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then + _info "Reloading: $DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" + if ! _docker_exec "$_cid" "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"; then + return 1 + fi + fi + return 0 +} + +#label +_get_id() { + _label="$1" + if [ "$_USE_DOCKER_COMMAND" ]; then + docker ps -f label="$_label" --format "{{.ID}}" + elif [ "$_USE_REST" ]; then + _err "Not implemented yet." + return 1 + elif [ "$_USE_UNIX_SOCKET" ]; then + _req="{\"label\":[\"$_label\"]}" + _debug2 _req "$_req" + _req="$(printf "%s" "$_req" | _url_encode)" + _debug2 _req "$_req" + listjson="$(_curl_unix_sock "${_DOCKER_SOCK:-$_DOCKER_HOST_DEFAULT}" GET "/containers/json?filters=$_req")" + _debug2 "listjson" "$listjson" + echo "$listjson" | tr '{,' '\n' | grep -i '"id":' | _head_n 1 | cut -d '"' -f 4 + else + _err "Not implemented yet." + return 1 + fi +} + +#id cmd +_docker_exec() { + _eargs="$*" + _debug2 "_docker_exec $_eargs" + _dcid="$1" + shift + if [ "$_USE_DOCKER_COMMAND" ]; then + docker exec -i "$_dcid" sh -c "$*" + elif [ "$_USE_REST" ]; then + _err "Not implemented yet." + return 1 + elif [ "$_USE_UNIX_SOCKET" ]; then + _cmd="$*" + #_cmd="$(printf "%s" "$_cmd" | sed 's/ /","/g')" + _debug2 _cmd "$_cmd" + #create exec instance: + cjson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/containers/$_dcid/exec" "{\"Cmd\": [\"sh\", \"-c\", \"$_cmd\"]}")" + _debug2 cjson "$cjson" + execid="$(echo "$cjson" | cut -d '"' -f 4)" + _debug execid "$execid" + ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": false,\"Tty\": false}")" + _debug2 ejson "$ejson" + if [ "$ejson" ]; then + _err "$ejson" + return 1 + fi + else + _err "Not implemented yet." + return 1 + fi +} + +#id from to +_docker_cp() { + _dcid="$1" + _from="$2" + _to="$3" + _info "Copying file from $_from to $_to" + _dir="$(dirname "$_to")" + _debug2 _dir "$_dir" + if ! _docker_exec "$_dcid" mkdir -p "$_dir"; then + _err "Can not create dir: $_dir" + return 1 + fi + if [ "$_USE_DOCKER_COMMAND" ]; then + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _docker_exec "$_dcid" tee "$_to" <"$_from" + else + _docker_exec "$_dcid" tee "$_to" <"$_from" >/dev/null + fi + if [ "$?" = "0" ]; then + _info "Success" + return 0 + else + _info "Error" + return 1 + fi + elif [ "$_USE_REST" ]; then + _err "Not implemented yet." + return 1 + elif [ "$_USE_UNIX_SOCKET" ]; then + _frompath="$_from" + if _startswith "$_frompath" '/'; then + _frompath="$(echo "$_from" | cut -b 2-)" #remove the first '/' char + fi + _debug2 "_frompath" "$_frompath" + _toname="$(basename "$_to")" + _debug2 "_toname" "$_toname" + _debug2 "_from" "$_from" + if ! tar --transform="s,$(printf "%s" "$_frompath" | tr '*' .),$_toname," -cz "$_from" 2>/dev/null | _curl_unix_sock "$_DOCKER_SOCK" PUT "/containers/$_dcid/archive?noOverwriteDirNonDir=1&path=$(printf "%s" "$_dir" | _url_encode)" '@-' "Content-Type: application/octet-stream"; then + _err "copy error" + return 1 + fi + return 0 + else + _err "Not implemented yet." + return 1 + fi + +} + +#sock method endpoint data content-type +_curl_unix_sock() { + _socket="$1" + _method="$2" + _endpoint="$3" + _data="$4" + _ctype="$5" + if [ -z "$_ctype" ]; then + _ctype="Content-Type: application/json" + fi + _debug _data "$_data" + _debug2 "url" "http://localhost$_endpoint" + if [ "$_CURL_NO_HOST" ]; then + _cux_url="http:$_endpoint" + else + _cux_url="http://localhost$_endpoint" + fi + + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + curl -vvv --silent --unix-socket "$_socket" -X "$_method" --data-binary "$_data" --header "$_ctype" "$_cux_url" + else + curl --silent --unix-socket "$_socket" -X "$_method" --data-binary "$_data" --header "$_ctype" "$_cux_url" + fi + +} + +_check_curl_version() { + _cversion="$(curl -V | grep '^curl ' | cut -d ' ' -f 2)" + _debug2 "_cversion" "$_cversion" + + _major="$(_getfield "$_cversion" 1 '.')" + _debug2 "_major" "$_major" + + _minor="$(_getfield "$_cversion" 2 '.')" + _debug2 "_minor" "$_minor" + + if [ "$_major" -ge "8" ]; then + #ok + return 0 + fi + if [ "$_major" = "7" ]; then + if [ "$_minor" -lt "40" ]; then + _err "curl v$_cversion doesn't support unit socket" + _err "Please upgrade to curl 7.40 or later." + return 1 + fi + if [ "$_minor" -lt "50" ]; then + _debug "Use short host name" + export _CURL_NO_HOST=1 + else + export _CURL_NO_HOST= + fi + return 0 + else + _err "curl v$_cversion doesn't support unit socket" + _err "Please upgrade to curl 7.40 or later." + return 1 + fi + +} diff --git a/deploy/exim4.sh b/deploy/exim4.sh index 573f762b..260b8798 100644 --- a/deploy/exim4.sh +++ b/deploy/exim4.sh @@ -69,8 +69,8 @@ exim4_deploy() { cp "$_exim4_conf" "$_backup_conf" _info "Modify exim4 conf: $_exim4_conf" - if _setopt "$_exim4_conf" "tls_certificate" "=" "$_real_fullchain" \ - && _setopt "$_exim4_conf" "tls_privatekey" "=" "$_real_key"; then + if _setopt "$_exim4_conf" "tls_certificate" "=" "$_real_fullchain" && + _setopt "$_exim4_conf" "tls_privatekey" "=" "$_real_key"; then _info "Set config success!" else _err "Config exim4 server error, please report bug to us." diff --git a/deploy/fritzbox.sh b/deploy/fritzbox.sh index 21ea6cfd..416a4121 100644 --- a/deploy/fritzbox.sh +++ b/deploy/fritzbox.sh @@ -28,47 +28,59 @@ fritzbox_deploy() { _debug _cfullchain "$_cfullchain" if ! _exists iconv; then - if ! _exists perl; then - _err "iconv or perl not found" - return 1 + if ! _exists uconv; then + if ! _exists perl; then + _err "iconv or uconv or perl not found" + return 1 + fi fi fi - _fritzbox_username="${DEPLOY_FRITZBOX_USERNAME}" - _fritzbox_password="${DEPLOY_FRITZBOX_PASSWORD}" - _fritzbox_url="${DEPLOY_FRITZBOX_URL}" + # Clear traces of incorrectly stored values + _clearaccountconf DEPLOY_FRITZBOX_USERNAME + _clearaccountconf DEPLOY_FRITZBOX_PASSWORD + _clearaccountconf DEPLOY_FRITZBOX_URL - _debug _fritzbox_url "$_fritzbox_url" - _debug _fritzbox_username "$_fritzbox_username" - _secure_debug _fritzbox_password "$_fritzbox_password" - if [ -z "$_fritzbox_username" ]; then + # Read config from saved values or env + _getdeployconf DEPLOY_FRITZBOX_USERNAME + _getdeployconf DEPLOY_FRITZBOX_PASSWORD + _getdeployconf DEPLOY_FRITZBOX_URL + + _debug DEPLOY_FRITZBOX_URL "$DEPLOY_FRITZBOX_URL" + _debug DEPLOY_FRITZBOX_USERNAME "$DEPLOY_FRITZBOX_USERNAME" + _secure_debug DEPLOY_FRITZBOX_PASSWORD "$DEPLOY_FRITZBOX_PASSWORD" + + if [ -z "$DEPLOY_FRITZBOX_USERNAME" ]; then _err "FRITZ!Box username is not found, please define DEPLOY_FRITZBOX_USERNAME." return 1 fi - if [ -z "$_fritzbox_password" ]; then + if [ -z "$DEPLOY_FRITZBOX_PASSWORD" ]; then _err "FRITZ!Box password is not found, please define DEPLOY_FRITZBOX_PASSWORD." return 1 fi - if [ -z "$_fritzbox_url" ]; then + if [ -z "$DEPLOY_FRITZBOX_URL" ]; then _err "FRITZ!Box url is not found, please define DEPLOY_FRITZBOX_URL." return 1 fi - _saveaccountconf DEPLOY_FRITZBOX_USERNAME "${_fritzbox_username}" - _saveaccountconf DEPLOY_FRITZBOX_PASSWORD "${_fritzbox_password}" - _saveaccountconf DEPLOY_FRITZBOX_URL "${_fritzbox_url}" + # Save current values + _savedeployconf DEPLOY_FRITZBOX_USERNAME "$DEPLOY_FRITZBOX_USERNAME" + _savedeployconf DEPLOY_FRITZBOX_PASSWORD "$DEPLOY_FRITZBOX_PASSWORD" + _savedeployconf DEPLOY_FRITZBOX_URL "$DEPLOY_FRITZBOX_URL" # Do not check for a valid SSL certificate, because initially the cert is not valid, so it could not install the LE generated certificate export HTTPS_INSECURE=1 _info "Log in to the FRITZ!Box" - _fritzbox_challenge="$(_get "${_fritzbox_url}/login_sid.lua" | sed -e 's/^.*//' -e 's/<\/Challenge>.*$//')" + _fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*//' -e 's/<\/Challenge>.*$//')" if _exists iconv; then - _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${_fritzbox_password}" | iconv -f ASCII -t UTF16LE | md5sum | awk '{print $1}')" + _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF16LE | _digest md5 hex)" + elif _exists uconv; then + _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF16LE | _digest md5 hex)" else - _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${_fritzbox_password}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | md5sum | awk '{print $1}')" + _fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | _digest md5 hex)" fi - _fritzbox_sid="$(_get "${_fritzbox_url}/login_sid.lua?sid=0000000000000000&username=${_fritzbox_username}&response=${_fritzbox_challenge}-${_fritzbox_hash}" | sed -e 's/^.*//' -e 's/<\/SID>.*$//')" + _fritzbox_sid="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua?sid=0000000000000000&username=${DEPLOY_FRITZBOX_USERNAME}&response=${_fritzbox_challenge}-${_fritzbox_hash}" | sed -e 's/^.*//' -e 's/<\/SID>.*$//')" if [ -z "${_fritzbox_sid}" ] || [ "${_fritzbox_sid}" = "0000000000000000" ]; then _err "Logging in to the FRITZ!Box failed. Please check username, password and URL." @@ -100,7 +112,7 @@ fritzbox_deploy() { _info "Upload certificate to the FRITZ!Box" export _H1="Content-type: multipart/form-data boundary=${_post_boundary}" - _post "$(cat "${_post_request}")" "${_fritzbox_url}/cgi-bin/firmwarecfg" | grep SSL + _post "$(cat "${_post_request}")" "${DEPLOY_FRITZBOX_URL}/cgi-bin/firmwarecfg" | grep SSL retval=$? if [ $retval = 0 ]; then diff --git a/deploy/gcore_cdn.sh b/deploy/gcore_cdn.sh new file mode 100644 index 00000000..fd17cc25 --- /dev/null +++ b/deploy/gcore_cdn.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env sh + +# Here is the script to deploy the cert to G-Core CDN service (https://gcore.com/) using the G-Core Labs API (https://apidocs.gcore.com/cdn). +# Returns 0 when success. +# +# Written by temoffey +# Public domain, 2019 +# Update by DreamOfIce in 2023 + +#export DEPLOY_GCORE_CDN_USERNAME=myusername +#export DEPLOY_GCORE_CDN_PASSWORD=mypassword + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain + +gcore_cdn_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + _fullchain=$(tr '\r\n' '*#' <"$_cfullchain" | sed 's/*#/#/g;s/##/#/g;s/#/\\n/g') + _key=$(tr '\r\n' '*#' <"$_ckey" | sed 's/*#/#/g;s/#/\\n/g') + + _debug _fullchain "$_fullchain" + _debug _key "$_key" + + if [ -z "$DEPLOY_GCORE_CDN_USERNAME" ]; then + if [ -z "$Le_Deploy_gcore_cdn_username" ]; then + _err "Please define the target username: export DEPLOY_GCORE_CDN_USERNAME=username" + return 1 + fi + else + Le_Deploy_gcore_cdn_username="$DEPLOY_GCORE_CDN_USERNAME" + _savedomainconf Le_Deploy_gcore_cdn_username "$Le_Deploy_gcore_cdn_username" + fi + + if [ -z "$DEPLOY_GCORE_CDN_PASSWORD" ]; then + if [ -z "$Le_Deploy_gcore_cdn_password" ]; then + _err "Please define the target password: export DEPLOY_GCORE_CDN_PASSWORD=password" + return 1 + fi + else + Le_Deploy_gcore_cdn_password="$DEPLOY_GCORE_CDN_PASSWORD" + _savedomainconf Le_Deploy_gcore_cdn_password "$Le_Deploy_gcore_cdn_password" + fi + + _info "Get authorization token" + _request="{\"username\":\"$Le_Deploy_gcore_cdn_username\",\"password\":\"$Le_Deploy_gcore_cdn_password\"}" + _debug _request "$_request" + export _H1="Content-Type:application/json" + _response=$(_post "$_request" "https://api.gcore.com/auth/jwt/login") + _debug _response "$_response" + _regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$" + _debug _regex "$_regex" + _token=$(echo "$_response" | sed -n "s/$_regex/\1/p") + _debug _token "$_token" + + if [ -z "$_token" ]; then + _err "Error G-Core Labs API authorization" + return 1 + fi + + _info "Find CDN resource with cname $_cdomain" + export _H2="Authorization:Bearer $_token" + _response=$(_get "https://api.gcore.com/cdn/resources") + _debug _response "$_response" + _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" | _egrep_o "$_regex") + _debug _resource "$_resource" + _regex=".*\"id\":\([0-9]*\).*$" + _debug _regex "$_regex" + _resourceId=$(echo "$_resource" | sed -n "s/$_regex/\1/p") + _debug _resourceId "$_resourceId" + _regex=".*\"sslData\":\([0-9]*\).*$" + _debug _regex "$_regex" + _sslDataOld=$(echo "$_resource" | sed -n "s/$_regex/\1/p") + _debug _sslDataOld "$_sslDataOld" + _regex=".*\"originGroup\":\([0-9]*\).*$" + _debug _regex "$_regex" + _originGroup=$(echo "$_resource" | sed -n "s/$_regex/\1/p") + _debug _originGroup "$_originGroup" + + if [ -z "$_resourceId" ] || [ -z "$_originGroup" ]; then + _err "Not found CDN resource with cname $_cdomain" + return 1 + fi + + _info "Add new SSL certificate" + _date=$(date "+%d.%m.%Y %H:%M:%S") + _request="{\"name\":\"$_cdomain ($_date)\",\"sslCertificate\":\"$_fullchain\",\"sslPrivateKey\":\"$_key\"}" + _debug _request "$_request" + _response=$(_post "$_request" "https://api.gcore.com/cdn/sslData") + _debug _response "$_response" + _regex=".*\"id\":\([0-9]*\).*$" + _debug _regex "$_regex" + _sslDataAdd=$(echo "$_response" | sed -n "s/$_regex/\1/p") + _debug _sslDataAdd "$_sslDataAdd" + + if [ -z "$_sslDataAdd" ]; then + _err "Error new SSL certificate add" + return 1 + fi + + _info "Update CDN resource" + _request="{\"originGroup\":$_originGroup,\"sslData\":$_sslDataAdd}" + _debug _request "$_request" + _response=$(_post "$_request" "https://api.gcore.com/cdn/resources/$_resourceId" '' "PUT") + _debug _response "$_response" + _regex=".*\"sslData\":\([0-9]*\).*$" + _debug _regex "$_regex" + _sslDataNew=$(echo "$_response" | sed -n "s/$_regex/\1/p") + _debug _sslDataNew "$_sslDataNew" + + if [ "$_sslDataNew" != "$_sslDataAdd" ]; then + _err "Error CDN resource update" + return 1 + fi + + if [ -z "$_sslDataOld" ] || [ "$_sslDataOld" = "null" ]; then + _info "Not found old SSL certificate" + else + _info "Delete old SSL certificate" + _response=$(_post '' "https://api.gcore.com/cdn/sslData/$_sslDataOld" '' "DELETE") + _debug _response "$_response" + fi + + _info "Certificate successfully deployed" + return 0 +} diff --git a/deploy/gitlab.sh b/deploy/gitlab.sh index ba2d3122..595b6d20 100644 --- a/deploy/gitlab.sh +++ b/deploy/gitlab.sh @@ -67,7 +67,7 @@ gitlab_deploy() { error_response="error" - if test "${_response#*$error_response}" != "$_response"; then + if test "${_response#*"$error_response"}" != "$_response"; then _err "Error in deploying certificate:" _err "$_response" return 1 diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index 5c1a40e2..d638abb8 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -1,8 +1,41 @@ #!/usr/bin/env sh -#Here is a script to deploy cert to haproxy server. - -#returns 0 means success, otherwise error. +# Script for acme.sh to deploy certificates to haproxy +# +# The following variables can be exported: +# +# export DEPLOY_HAPROXY_PEM_NAME="${domain}.pem" +# +# Defines the name of the PEM file. +# Defaults to ".pem" +# +# export DEPLOY_HAPROXY_PEM_PATH="/etc/haproxy" +# +# Defines location of PEM file for HAProxy. +# Defaults to /etc/haproxy +# +# export DEPLOY_HAPROXY_RELOAD="systemctl reload haproxy" +# +# 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_HAPROXY_ISSUER="no" +# +# OPTIONAL: Places CA file as "${DEPLOY_HAPROXY_PEM}.issuer" +# Note: Required for OCSP stapling to work +# +# export DEPLOY_HAPROXY_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 +# HAProxy will load all certificates and provide one or the other +# depending on client capabilities +# Note: This functionality requires HAProxy was compiled against +# a version of OpenSSL that supports this. +# ######## Public functions ##################### @@ -14,45 +47,234 @@ haproxy_deploy() { _cca="$4" _cfullchain="$5" - _debug _cdomain "$_cdomain" - _debug _ckey "$_ckey" - _debug _ccert "$_ccert" - _debug _cca "$_cca" - _debug _cfullchain "$_cfullchain" + # Some defaults + DEPLOY_HAPROXY_PEM_PATH_DEFAULT="/etc/haproxy" + DEPLOY_HAPROXY_PEM_NAME_DEFAULT="${_cdomain}.pem" + DEPLOY_HAPROXY_BUNDLE_DEFAULT="no" + DEPLOY_HAPROXY_ISSUER_DEFAULT="no" + DEPLOY_HAPROXY_RELOAD_DEFAULT="true" - # handle reload preference - DEFAULT_HAPROXY_RELOAD="/usr/sbin/service haproxy restart" - if [ -z "${DEPLOY_HAPROXY_RELOAD}" ]; then - _reload="${DEFAULT_HAPROXY_RELOAD}" - _cleardomainconf DEPLOY_HAPROXY_RELOAD - else - _reload="${DEPLOY_HAPROXY_RELOAD}" - _savedomainconf DEPLOY_HAPROXY_RELOAD "$DEPLOY_HAPROXY_RELOAD" + _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_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}" + elif [ -z "${Le_Deploy_haproxy_pem_path}" ]; then + Le_Deploy_haproxy_pem_path="${DEPLOY_HAPROXY_PEM_PATH_DEFAULT}" fi - _savedomainconf DEPLOY_HAPROXY_PEM_PATH "$DEPLOY_HAPROXY_PEM_PATH" - # work out the path where the PEM file should go - _pem_path="${DEPLOY_HAPROXY_PEM_PATH}" - if [ -z "$_pem_path" ]; then - _err "Path to save PEM file not found. Please define DEPLOY_HAPROXY_PEM_PATH." - return 1 - fi - _pem_full_path="$_pem_path/$_cdomain.pem" - _info "Full path to PEM $_pem_full_path" - - # combine the key and fullchain into a single pem and install - cat "$_cfullchain" "$_ckey" >"$_pem_full_path" - chmod 600 "$_pem_full_path" - _info "Certificate successfully deployed" - - # restart HAProxy - _info "Run reload: $_reload" - if eval "$_reload"; then - _info "Reload success!" - return 0 + # Ensure PEM_PATH exists + if [ -d "${Le_Deploy_haproxy_pem_path}" ]; then + _debug "PEM_PATH ${Le_Deploy_haproxy_pem_path} exists" else - _err "Reload error" + _err "PEM_PATH ${Le_Deploy_haproxy_pem_path} does not exist" return 1 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}" + elif [ -z "${Le_Deploy_haproxy_pem_name}" ]; then + Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}" + 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}" + elif [ -z "${Le_Deploy_haproxy_bundle}" ]; then + Le_Deploy_haproxy_bundle="${DEPLOY_HAPROXY_BUNDLE_DEFAULT}" + 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}" + elif [ -z "${Le_Deploy_haproxy_issuer}" ]; then + Le_Deploy_haproxy_issuer="${DEPLOY_HAPROXY_ISSUER_DEFAULT}" + 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}" + elif [ -z "${Le_Deploy_haproxy_reload}" ]; then + Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD_DEFAULT}" + fi + + # Set the suffix depending if we are creating a bundle or not + if [ "${Le_Deploy_haproxy_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_haproxy_pem_path}/${Le_Deploy_haproxy_pem_name}${_suffix}" + _issuer="${_pem}.issuer" + _ocsp="${_pem}.ocsp" + _reload="${Le_Deploy_haproxy_reload}" + + _info "Deploying PEM file" + # Create a temporary PEM file + _temppem="$(_mktemp)" + _debug _temppem "${_temppem}" + cat "${_ccert}" "${_cca}" "${_ckey}" >"${_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_haproxy_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 HAProxy just ignores it in this case + # rm -f "${_ocsp}" || _err "Problem removing stale .ocsp file" + fi + fi + + # Reload HAProxy + _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/kong.sh b/deploy/kong.sh index d3a6bc47..b8facedf 100755 --- a/deploy/kong.sh +++ b/deploy/kong.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -# If certificate already exist it will update only cert and key not touching other parameter -# If certificate doesn't exist it will only upload cert and key and not set other parameter +# If certificate already exists it will update only cert and key, not touching other parameters +# If certificate doesn't exist it will only upload cert and key, and not set other parameters # Note that we deploy full chain # Written by Geoffroi Genot @@ -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/mailcow.sh b/deploy/mailcow.sh new file mode 100644 index 00000000..99dc80e2 --- /dev/null +++ b/deploy/mailcow.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env sh + +#Here is a script to deploy cert to mailcow. + +#returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +mailcow_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + _getdeployconf DEPLOY_MAILCOW_PATH + _getdeployconf DEPLOY_MAILCOW_RELOAD + + _debug DEPLOY_MAILCOW_PATH "$DEPLOY_MAILCOW_PATH" + _debug DEPLOY_MAILCOW_RELOAD "$DEPLOY_MAILCOW_RELOAD" + + if [ -z "$DEPLOY_MAILCOW_PATH" ]; then + _err "Mailcow path is not found, please define DEPLOY_MAILCOW_PATH." + return 1 + fi + + _savedeployconf DEPLOY_MAILCOW_PATH "$DEPLOY_MAILCOW_PATH" + [ -n "$DEPLOY_MAILCOW_RELOAD" ] && _savedeployconf DEPLOY_MAILCOW_RELOAD "$DEPLOY_MAILCOW_RELOAD" + + _ssl_path="$DEPLOY_MAILCOW_PATH" + if [ -f "$DEPLOY_MAILCOW_PATH/generate_config.sh" ]; then + _ssl_path="$DEPLOY_MAILCOW_PATH/data/assets/ssl/" + fi + + if [ ! -d "$_ssl_path" ]; then + _err "Cannot find mailcow ssl path: $_ssl_path" + return 1 + fi + + _info "Copying key and cert" + _real_key="$_ssl_path/key.pem" + if ! cat "$_ckey" >"$_real_key"; then + _err "Error: write key file to: $_real_key" + return 1 + fi + + _real_fullchain="$_ssl_path/cert.pem" + if ! cat "$_cfullchain" >"$_real_fullchain"; then + _err "Error: write cert file to: $_real_fullchain" + return 1 + fi + + DEFAULT_MAILCOW_RELOAD="docker restart \$(docker ps --quiet --filter name=nginx-mailcow --filter name=dovecot-mailcow --filter name=postfix-mailcow)" + _reload="${DEPLOY_MAILCOW_RELOAD:-$DEFAULT_MAILCOW_RELOAD}" + + _info "Run reload: $_reload" + if eval "$_reload"; then + _info "Reload success!" + fi + return 0 + +} diff --git a/deploy/mydevil.sh b/deploy/mydevil.sh new file mode 100755 index 00000000..bd9868aa --- /dev/null +++ b/deploy/mydevil.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env sh + +# MyDevil.net API (2019-02-03) +# +# MyDevil.net already supports automatic Let's Encrypt certificates, +# except for wildcard domains. +# +# This script depends on `devil` command that MyDevil.net provides, +# which means that it works only on server side. +# +# Author: Marcin Konicki +# +######## Public functions ##################### + +# Usage: mydevil_deploy domain keyfile certfile cafile fullchain +mydevil_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + ip="" + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + if ! _exists "devil"; then + _err "Could not find 'devil' command." + return 1 + fi + + ip=$(mydevil_get_ip "$_cdomain") + if [ -z "$ip" ]; then + _err "Could not find IP for domain $_cdomain." + return 1 + fi + + # Delete old certificate first + _info "Removing old certificate for $_cdomain at $ip" + devil ssl www del "$ip" "$_cdomain" + + # Add new certificate + _info "Adding new certificate for $_cdomain at $ip" + devil ssl www add "$ip" "$_cfullchain" "$_ckey" "$_cdomain" || return 1 + + return 0 +} + +#################### Private functions below ################################## + +# Usage: ip=$(mydevil_get_ip domain.com) +# echo $ip +mydevil_get_ip() { + devil dns list "$1" | cut -w -s -f 3,7 | grep "^A$(printf '\t')" | cut -w -s -f 2 || return 1 + return 0 +} diff --git a/deploy/openmediavault.sh b/deploy/openmediavault.sh new file mode 100644 index 00000000..cfc2d332 --- /dev/null +++ b/deploy/openmediavault.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env sh + +# This deploy hook is tested on OpenMediaVault 5.x. It supports both local and remote deployment. +# The way it works is that if a cert with the matching domain name is not found, it will firstly create a dummy cert to get its uuid, and then replace it with your cert. +# +# DEPLOY_OMV_WEBUI_ADMIN - This is OMV web gui admin account. Default value is admin. It's required as the user parameter (-u) for the omv-rpc command. +# DEPLOY_OMV_HOST and DEPLOY_OMV_SSH_USER are optional. They are used for remote deployment through ssh (support public key authentication only). Per design, OMV web gui admin doesn't have ssh permission, so another account is needed for ssh. +# +# returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +openmediavault_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + _getdeployconf DEPLOY_OMV_WEBUI_ADMIN + + if [ -z "$DEPLOY_OMV_WEBUI_ADMIN" ]; then + DEPLOY_OMV_WEBUI_ADMIN="admin" + fi + + _savedeployconf DEPLOY_OMV_WEBUI_ADMIN "$DEPLOY_OMV_WEBUI_ADMIN" + + _getdeployconf DEPLOY_OMV_HOST + _getdeployconf DEPLOY_OMV_SSH_USER + + if [ -n "$DEPLOY_OMV_HOST" ] && [ -n "$DEPLOY_OMV_SSH_USER" ]; then + _info "[OMV deploy-hook] Deploy certificate remotely through ssh." + _savedeployconf DEPLOY_OMV_HOST "$DEPLOY_OMV_HOST" + _savedeployconf DEPLOY_OMV_SSH_USER "$DEPLOY_OMV_SSH_USER" + else + _info "[OMV deploy-hook] Deploy certificate locally." + fi + + if [ -n "$DEPLOY_OMV_HOST" ] && [ -n "$DEPLOY_OMV_SSH_USER" ]; then + + _command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'getList' '{\"start\": 0, \"limit\": -1}' | jq -r '.data[] | select(.name==\"/CN='$_cdomain'\") | .uuid'" + # shellcheck disable=SC2029 + _uuid=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command") + _debug _command "$_command" + + if [ -z "$_uuid" ]; then + _info "[OMV deploy-hook] Domain $_cdomain has no certificate in openmediavault, creating it!" + _command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'create' '{\"cn\": \"test.example.com\", \"size\": 4096, \"days\": 3650, \"c\": \"\", \"st\": \"\", \"l\": \"\", \"o\": \"\", \"ou\": \"\", \"email\": \"\"}' | jq -r '.uuid'" + # shellcheck disable=SC2029 + _uuid=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command") + _debug _command "$_command" + + if [ -z "$_uuid" ]; then + _err "[OMV deploy-hook] An error occured while creating the certificate" + return 1 + fi + fi + + _info "[OMV deploy-hook] Domain $_cdomain has uuid: $_uuid" + _fullchain=$(jq <"$_cfullchain" -aRs .) + _key=$(jq <"$_ckey" -aRs .) + + _debug _fullchain "$_fullchain" + _debug _key "$_key" + + _info "[OMV deploy-hook] Updating key and certificate in openmediavault" + _command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'set' '{\"uuid\":\"$_uuid\", \"certificate\":$_fullchain, \"privatekey\":$_key, \"comment\":\"acme.sh deployed $(date)\"}'" + # shellcheck disable=SC2029 + _result=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command") + + _debug _command "$_command" + _debug _result "$_result" + + _command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'WebGui' 'setSettings' \$(omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'WebGui' 'getSettings' | jq -c '.sslcertificateref=\"$_uuid\"')" + # shellcheck disable=SC2029 + _result=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command") + + _debug _command "$_command" + _debug _result "$_result" + + _info "[OMV deploy-hook] Asking openmediavault to apply changes... (this could take some time, hang in there)" + _command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'Config' 'applyChanges' '{\"modules\":[], \"force\": false}'" + # shellcheck disable=SC2029 + _result=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command") + + _debug _command "$_command" + _debug _result "$_result" + + _info "[OMV deploy-hook] Asking nginx to reload" + _command="nginx -s reload" + # shellcheck disable=SC2029 + _result=$(ssh "$DEPLOY_OMV_SSH_USER@$DEPLOY_OMV_HOST" "$_command") + + _debug _command "$_command" + _debug _result "$_result" + + else + + # shellcheck disable=SC2086 + _uuid=$(omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'getList' '{"start": 0, "limit": -1}' | jq -r '.data[] | select(.name=="/CN='$_cdomain'") | .uuid') + if [ -z "$_uuid" ]; then + _info "[OMV deploy-hook] Domain $_cdomain has no certificate in openmediavault, creating it!" + # shellcheck disable=SC2086 + _uuid=$(omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'create' '{"cn": "test.example.com", "size": 4096, "days": 3650, "c": "", "st": "", "l": "", "o": "", "ou": "", "email": ""}' | jq -r '.uuid') + + if [ -z "$_uuid" ]; then + _err "[OMB deploy-hook] An error occured while creating the certificate" + return 1 + fi + fi + + _info "[OMV deploy-hook] Domain $_cdomain has uuid: $_uuid" + _fullchain=$(jq <"$_cfullchain" -aRs .) + _key=$(jq <"$_ckey" -aRs .) + + _debug _fullchain "$_fullchain" + _debug _key "$_key" + + _info "[OMV deploy-hook] Updating key and certificate in openmediavault" + _command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'CertificateMgmt' 'set' '{\"uuid\":\"$_uuid\", \"certificate\":$_fullchain, \"privatekey\":$_key, \"comment\":\"acme.sh deployed $(date)\"}'" + _result=$(eval "$_command") + + _debug _command "$_command" + _debug _result "$_result" + + _command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'WebGui' 'setSettings' \$(omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'WebGui' 'getSettings' | jq -c '.sslcertificateref=\"$_uuid\"')" + _result=$(eval "$_command") + + _debug _command "$_command" + _debug _result "$_result" + + _info "[OMV deploy-hook] Asking openmediavault to apply changes... (this could take some time, hang in there)" + _command="omv-rpc -u $DEPLOY_OMV_WEBUI_ADMIN 'Config' 'applyChanges' '{\"modules\":[], \"force\": false}'" + _result=$(eval "$_command") + + _debug _command "$_command" + _debug _result "$_result" + + _info "[OMV deploy-hook] Asking nginx to reload" + _command="nginx -s reload" + _result=$(eval "$_command") + + _debug _command "$_command" + _debug _result "$_result" + + fi + + return 0 +} diff --git a/deploy/openstack.sh b/deploy/openstack.sh new file mode 100644 index 00000000..f2058853 --- /dev/null +++ b/deploy/openstack.sh @@ -0,0 +1,262 @@ +#!/usr/bin/env sh + +# OpenStack Barbican deploy hook +# +# This requires you to have OpenStackClient and python-barbicanclient +# installed. +# +# You will require Keystone V3 credentials loaded into your environment, which +# could be either password or v3applicationcredential type. +# +# Author: Andy Botting + +openstack_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + if ! _exists openstack; then + _err "OpenStack client not found" + return 1 + fi + + _openstack_credentials || return $? + + _info "Generate import pkcs12" + _import_pkcs12="$(_mktemp)" + if ! _openstack_to_pkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca"; then + _err "Error creating pkcs12 certificate" + return 1 + fi + _debug _import_pkcs12 "$_import_pkcs12" + _base64_pkcs12=$(_base64 "multiline" <"$_import_pkcs12") + + secretHrefs=$(_openstack_get_secrets) + _debug secretHrefs "$secretHrefs" + _openstack_store_secret || return $? + + if [ -n "$secretHrefs" ]; then + _info "Cleaning up existing secret" + _openstack_delete_secrets || return $? + fi + + _info "Certificate successfully deployed" + return 0 +} + +_openstack_store_secret() { + if ! openstack secret store --name "$_cdomain." -t 'application/octet-stream' -e base64 --payload "$_base64_pkcs12"; then + _err "Failed to create OpenStack secret" + return 1 + fi + return +} + +_openstack_delete_secrets() { + echo "$secretHrefs" | while read -r secretHref; do + _info "Deleting old secret $secretHref" + if ! openstack secret delete "$secretHref"; then + _err "Failed to delete OpenStack secret" + return 1 + fi + done + return +} + +_openstack_get_secrets() { + if ! secretHrefs=$(openstack secret list -f value --name "$_cdomain." | cut -d' ' -f1); then + _err "Failed to list secrets" + return 1 + fi + echo "$secretHrefs" +} + +_openstack_to_pkcs() { + # The existing _toPkcs command can't allow an empty password, due to sh + # -z test, so copied here and forcing the empty password. + _cpfx="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + + ${ACME_OPENSSL_BIN:-openssl} pkcs12 -export -out "$_cpfx" -inkey "$_ckey" -in "$_ccert" -certfile "$_cca" -password "pass:" +} + +_openstack_credentials() { + _debug "Check OpenStack credentials" + + # If we have OS_AUTH_URL already set in the environment, then assume we want + # to use those, otherwise use stored credentials + if [ -n "$OS_AUTH_URL" ]; then + _debug "OS_AUTH_URL env var found, using environment" + else + _debug "OS_AUTH_URL not found, loading stored credentials" + OS_AUTH_URL="${OS_AUTH_URL:-$(_readaccountconf_mutable OS_AUTH_URL)}" + OS_IDENTITY_API_VERSION="${OS_IDENTITY_API_VERSION:-$(_readaccountconf_mutable OS_IDENTITY_API_VERSION)}" + OS_AUTH_TYPE="${OS_AUTH_TYPE:-$(_readaccountconf_mutable OS_AUTH_TYPE)}" + OS_APPLICATION_CREDENTIAL_ID="${OS_APPLICATION_CREDENTIAL_ID:-$(_readaccountconf_mutable OS_APPLICATION_CREDENTIAL_ID)}" + OS_APPLICATION_CREDENTIAL_SECRET="${OS_APPLICATION_CREDENTIAL_SECRET:-$(_readaccountconf_mutable OS_APPLICATION_CREDENTIAL_SECRET)}" + OS_USERNAME="${OS_USERNAME:-$(_readaccountconf_mutable OS_USERNAME)}" + OS_PASSWORD="${OS_PASSWORD:-$(_readaccountconf_mutable OS_PASSWORD)}" + OS_PROJECT_NAME="${OS_PROJECT_NAME:-$(_readaccountconf_mutable OS_PROJECT_NAME)}" + OS_PROJECT_ID="${OS_PROJECT_ID:-$(_readaccountconf_mutable OS_PROJECT_ID)}" + OS_USER_DOMAIN_NAME="${OS_USER_DOMAIN_NAME:-$(_readaccountconf_mutable OS_USER_DOMAIN_NAME)}" + OS_USER_DOMAIN_ID="${OS_USER_DOMAIN_ID:-$(_readaccountconf_mutable OS_USER_DOMAIN_ID)}" + OS_PROJECT_DOMAIN_NAME="${OS_PROJECT_DOMAIN_NAME:-$(_readaccountconf_mutable OS_PROJECT_DOMAIN_NAME)}" + OS_PROJECT_DOMAIN_ID="${OS_PROJECT_DOMAIN_ID:-$(_readaccountconf_mutable OS_PROJECT_DOMAIN_ID)}" + fi + + # Check each var and either save or clear it depending on whether its set. + # The helps us clear out old vars in the case where a user may want + # to switch between password and app creds + _debug "OS_AUTH_URL" "$OS_AUTH_URL" + if [ -n "$OS_AUTH_URL" ]; then + export OS_AUTH_URL + _saveaccountconf_mutable OS_AUTH_URL "$OS_AUTH_URL" + else + unset OS_AUTH_URL + _clearaccountconf SAVED_OS_AUTH_URL + fi + + _debug "OS_IDENTITY_API_VERSION" "$OS_IDENTITY_API_VERSION" + if [ -n "$OS_IDENTITY_API_VERSION" ]; then + export OS_IDENTITY_API_VERSION + _saveaccountconf_mutable OS_IDENTITY_API_VERSION "$OS_IDENTITY_API_VERSION" + else + unset OS_IDENTITY_API_VERSION + _clearaccountconf SAVED_OS_IDENTITY_API_VERSION + fi + + _debug "OS_AUTH_TYPE" "$OS_AUTH_TYPE" + if [ -n "$OS_AUTH_TYPE" ]; then + export OS_AUTH_TYPE + _saveaccountconf_mutable OS_AUTH_TYPE "$OS_AUTH_TYPE" + else + unset OS_AUTH_TYPE + _clearaccountconf SAVED_OS_AUTH_TYPE + fi + + _debug "OS_APPLICATION_CREDENTIAL_ID" "$OS_APPLICATION_CREDENTIAL_ID" + if [ -n "$OS_APPLICATION_CREDENTIAL_ID" ]; then + export OS_APPLICATION_CREDENTIAL_ID + _saveaccountconf_mutable OS_APPLICATION_CREDENTIAL_ID "$OS_APPLICATION_CREDENTIAL_ID" + else + unset OS_APPLICATION_CREDENTIAL_ID + _clearaccountconf SAVED_OS_APPLICATION_CREDENTIAL_ID + fi + + _secure_debug "OS_APPLICATION_CREDENTIAL_SECRET" "$OS_APPLICATION_CREDENTIAL_SECRET" + if [ -n "$OS_APPLICATION_CREDENTIAL_SECRET" ]; then + export OS_APPLICATION_CREDENTIAL_SECRET + _saveaccountconf_mutable OS_APPLICATION_CREDENTIAL_SECRET "$OS_APPLICATION_CREDENTIAL_SECRET" + else + unset OS_APPLICATION_CREDENTIAL_SECRET + _clearaccountconf SAVED_OS_APPLICATION_CREDENTIAL_SECRET + fi + + _debug "OS_USERNAME" "$OS_USERNAME" + if [ -n "$OS_USERNAME" ]; then + export OS_USERNAME + _saveaccountconf_mutable OS_USERNAME "$OS_USERNAME" + else + unset OS_USERNAME + _clearaccountconf SAVED_OS_USERNAME + fi + + _secure_debug "OS_PASSWORD" "$OS_PASSWORD" + if [ -n "$OS_PASSWORD" ]; then + export OS_PASSWORD + _saveaccountconf_mutable OS_PASSWORD "$OS_PASSWORD" + else + unset OS_PASSWORD + _clearaccountconf SAVED_OS_PASSWORD + fi + + _debug "OS_PROJECT_NAME" "$OS_PROJECT_NAME" + if [ -n "$OS_PROJECT_NAME" ]; then + export OS_PROJECT_NAME + _saveaccountconf_mutable OS_PROJECT_NAME "$OS_PROJECT_NAME" + else + unset OS_PROJECT_NAME + _clearaccountconf SAVED_OS_PROJECT_NAME + fi + + _debug "OS_PROJECT_ID" "$OS_PROJECT_ID" + if [ -n "$OS_PROJECT_ID" ]; then + export OS_PROJECT_ID + _saveaccountconf_mutable OS_PROJECT_ID "$OS_PROJECT_ID" + else + unset OS_PROJECT_ID + _clearaccountconf SAVED_OS_PROJECT_ID + fi + + _debug "OS_USER_DOMAIN_NAME" "$OS_USER_DOMAIN_NAME" + if [ -n "$OS_USER_DOMAIN_NAME" ]; then + export OS_USER_DOMAIN_NAME + _saveaccountconf_mutable OS_USER_DOMAIN_NAME "$OS_USER_DOMAIN_NAME" + else + unset OS_USER_DOMAIN_NAME + _clearaccountconf SAVED_OS_USER_DOMAIN_NAME + fi + + _debug "OS_USER_DOMAIN_ID" "$OS_USER_DOMAIN_ID" + if [ -n "$OS_USER_DOMAIN_ID" ]; then + export OS_USER_DOMAIN_ID + _saveaccountconf_mutable OS_USER_DOMAIN_ID "$OS_USER_DOMAIN_ID" + else + unset OS_USER_DOMAIN_ID + _clearaccountconf SAVED_OS_USER_DOMAIN_ID + fi + + _debug "OS_PROJECT_DOMAIN_NAME" "$OS_PROJECT_DOMAIN_NAME" + if [ -n "$OS_PROJECT_DOMAIN_NAME" ]; then + export OS_PROJECT_DOMAIN_NAME + _saveaccountconf_mutable OS_PROJECT_DOMAIN_NAME "$OS_PROJECT_DOMAIN_NAME" + else + unset OS_PROJECT_DOMAIN_NAME + _clearaccountconf SAVED_OS_PROJECT_DOMAIN_NAME + fi + + _debug "OS_PROJECT_DOMAIN_ID" "$OS_PROJECT_DOMAIN_ID" + if [ -n "$OS_PROJECT_DOMAIN_ID" ]; then + export OS_PROJECT_DOMAIN_ID + _saveaccountconf_mutable OS_PROJECT_DOMAIN_ID "$OS_PROJECT_DOMAIN_ID" + else + unset OS_PROJECT_DOMAIN_ID + _clearaccountconf SAVED_OS_PROJECT_DOMAIN_ID + fi + + if [ "$OS_AUTH_TYPE" = "v3applicationcredential" ]; then + # Application Credential auth + if [ -z "$OS_APPLICATION_CREDENTIAL_ID" ] || [ -z "$OS_APPLICATION_CREDENTIAL_SECRET" ]; then + _err "When using OpenStack application credentials, OS_APPLICATION_CREDENTIAL_ID" + _err "and OS_APPLICATION_CREDENTIAL_SECRET must be set." + _err "Please check your credentials and try again." + return 1 + fi + else + # Password auth + if [ -z "$OS_USERNAME" ] || [ -z "$OS_PASSWORD" ]; then + _err "OpenStack username or password not found." + _err "Please check your credentials and try again." + return 1 + fi + + if [ -z "$OS_PROJECT_NAME" ] && [ -z "$OS_PROJECT_ID" ]; then + _err "When using password authentication, OS_PROJECT_NAME or" + _err "OS_PROJECT_ID must be set." + _err "Please check your credentials and try again." + return 1 + fi + fi + + return 0 +} diff --git a/deploy/panos.sh b/deploy/panos.sh new file mode 100644 index 00000000..89458e5f --- /dev/null +++ b/deploy/panos.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env sh + +# Script to deploy certificates to Palo Alto Networks PANOS via API +# Note PANOS API KEY and IP address needs to be set prior to running. +# The following variables exported from environment will be used. +# If not set then values previously saved in domain.conf file are used. +# +# Firewall admin with superuser and IP address is required. +# +# REQURED: +# export PANOS_HOST="" +# export PANOS_USER="" #User *MUST* have Commit and Import Permissions in XML API for Admin Role +# export PANOS_PASS="" +# +# The script will automatically generate a new API key if +# no key is found, or if a saved key has expired or is invalid. + +# This function is to parse the XML response from the firewall +parse_response() { + type=$2 + if [ "$type" = 'keygen' ]; then + status=$(echo "$1" | sed 's/^.*\(['\'']\)\([a-z]*\)'\''.*/\2/g') + if [ "$status" = "success" ]; then + panos_key=$(echo "$1" | sed 's/^.*\(\)\(.*\)<\/key>.*/\2/g') + _panos_key=$panos_key + else + message="PAN-OS Key could not be set." + fi + else + status=$(echo "$1" | tr -d '\n' | sed 's/^.*"\([a-z]*\)".*/\1/g') + message=$(echo "$1" | tr -d '\n' | sed 's/.*\(\|\|\)\([^<]*\).*/\2/g') + _debug "Firewall message: $message" + if [ "$type" = 'keytest' ] && [ "$status" != "success" ]; then + _debug "**** API Key has EXPIRED or is INVALID ****" + unset _panos_key + fi + fi + return 0 +} + +#This function is used to deploy to the firewall +deployer() { + content="" + type=$1 # Types are keytest, keygen, cert, key, commit + panos_url="https://$_panos_host/api/" + + #Test API Key by performing a lookup + if [ "$type" = 'keytest' ]; then + _debug "**** Testing saved API Key ****" + _H1="Content-Type: application/x-www-form-urlencoded" + # Get Version Info to test key + content="type=version&key=$_panos_key" + ## Exclude all scopes for the empty commit + #_exclude_scope="excludeexcludeexclude" + #content="type=commit&action=partial&key=$_panos_key&cmd=$_exclude_scopeacmekeytest" + fi + + # Generate API Key + if [ "$type" = 'keygen' ]; then + _debug "**** Generating new API Key ****" + _H1="Content-Type: application/x-www-form-urlencoded" + content="type=keygen&user=$_panos_user&password=$_panos_pass" + # content="$content${nl}--$delim${nl}Content-Disposition: form-data; type=\"keygen\"; user=\"$_panos_user\"; password=\"$_panos_pass\"${nl}Content-Type: application/octet-stream${nl}${nl}" + fi + + # Deploy Cert or Key + if [ "$type" = 'cert' ] || [ "$type" = 'key' ]; then + _debug "**** Deploying $type ****" + #Generate DELIM + delim="-----MultipartDelimiter$(date "+%s%N")" + nl="\015\012" + #Set Header + export _H1="Content-Type: multipart/form-data; boundary=$delim" + if [ "$type" = 'cert' ]; then + panos_url="${panos_url}?type=import" + content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\ncertificate" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")" + fi + if [ "$type" = 'key' ]; then + panos_url="${panos_url}?type=import" + content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\nprivate-key" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"passphrase\"\r\n\r\n123456" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cdomain.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")" + fi + #Close multipart + content="$content${nl}--$delim--${nl}${nl}" + #Convert CRLF + content=$(printf %b "$content") + fi + + # Commit changes + if [ "$type" = 'commit' ]; then + _debug "**** Committing changes ****" + export _H1="Content-Type: application/x-www-form-urlencoded" + #Check for force commit - will commit ALL uncommited changes to the firewall. Use with caution! + if [ "$FORCE" ]; then + _debug "Force switch detected. Committing ALL changes to the firewall." + cmd=$(printf "%s" "$_panos_user" | _url_encode) + else + _exclude_scope="excludeexclude" + cmd=$(printf "%s" "$_exclude_scope$_panos_user" | _url_encode) + fi + content="type=commit&action=partial&key=$_panos_key&cmd=$cmd" + fi + + response=$(_post "$content" "$panos_url" "" "POST") + parse_response "$response" "$type" + # Saving response to variables + response_status=$status + _debug response_status "$response_status" + if [ "$response_status" = "success" ]; then + _debug "Successfully deployed $type" + return 0 + else + _err "Deploy of type $type failed. Try deploying with --debug to troubleshoot." + _debug "$message" + return 1 + fi +} + +# This is the main function that will call the other functions to deploy everything. +panos_deploy() { + _cdomain=$(echo "$1" | sed 's/*/WILDCARD_/g') #Wildcard Safe Filename + _ckey="$2" + _cfullchain="$5" + + # VALID FILE CHECK + if [ ! -f "$_ckey" ] || [ ! -f "$_cfullchain" ]; then + _err "Unable to find a valid key and/or cert. If this is an ECDSA/ECC cert, use the --ecc flag when deploying." + return 1 + fi + + # PANOS_HOST + if [ "$PANOS_HOST" ]; then + _debug "Detected ENV variable PANOS_HOST. Saving to file." + _savedeployconf PANOS_HOST "$PANOS_HOST" 1 + else + _debug "Attempting to load variable PANOS_HOST from file." + _getdeployconf PANOS_HOST + fi + + # PANOS USER + if [ "$PANOS_USER" ]; then + _debug "Detected ENV variable PANOS_USER. Saving to file." + _savedeployconf PANOS_USER "$PANOS_USER" 1 + else + _debug "Attempting to load variable PANOS_USER from file." + _getdeployconf PANOS_USER + fi + + # PANOS_PASS + if [ "$PANOS_PASS" ]; then + _debug "Detected ENV variable PANOS_PASS. Saving to file." + _savedeployconf PANOS_PASS "$PANOS_PASS" 1 + else + _debug "Attempting to load variable PANOS_PASS from file." + _getdeployconf PANOS_PASS + fi + + # PANOS_KEY + _getdeployconf PANOS_KEY + if [ "$PANOS_KEY" ]; then + _debug "Detected saved key." + _panos_key=$PANOS_KEY + else + _debug "No key detected" + unset _panos_key + fi + + #Store variables + _panos_host=$PANOS_HOST + _panos_user=$PANOS_USER + _panos_pass=$PANOS_PASS + + #Test API Key if found. If the key is invalid, the variable _panos_key will be unset. + if [ "$_panos_host" ] && [ "$_panos_key" ]; then + _debug "**** Testing API KEY ****" + deployer keytest + fi + + # Check for valid variables + if [ -z "$_panos_host" ]; then + _err "No host found. If this is your first time deploying, please set PANOS_HOST in ENV variables. You can delete it after you have successfully deployed the certs." + return 1 + elif [ -z "$_panos_user" ]; then + _err "No user found. If this is your first time deploying, please set PANOS_USER in ENV variables. You can delete it after you have successfully deployed the certs." + return 1 + elif [ -z "$_panos_pass" ]; then + _err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs." + return 1 + else + # Generate a new API key if no valid API key is found + if [ -z "$_panos_key" ]; then + _debug "**** Generating new PANOS API KEY ****" + deployer keygen + _savedeployconf PANOS_KEY "$_panos_key" 1 + fi + + # Confirm that a valid key was generated + if [ -z "$_panos_key" ]; then + _err "Unable to generate an API key. The user and pass may be invalid or not authorized to generate a new key. Please check the PANOS_USER and PANOS_PASS credentials and try again" + return 1 + else + deployer cert + deployer key + deployer commit + fi + fi +} diff --git a/deploy/peplink.sh b/deploy/peplink.sh new file mode 100644 index 00000000..c4bd6242 --- /dev/null +++ b/deploy/peplink.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env sh + +# Script to deploy cert to Peplink Routers +# +# The following environment variables must be set: +# +# PEPLINK_Hostname - Peplink hostname +# PEPLINK_Username - Peplink username to login +# PEPLINK_Password - Peplink password to login +# +# The following environmental variables may be set if you don't like their +# default values: +# +# PEPLINK_Certtype - Certificate type to target for replacement +# defaults to "webadmin", can be one of: +# * "chub" (ContentHub) +# * "openvpn" (OpenVPN CA) +# * "portal" (Captive Portal SSL) +# * "webadmin" (Web Admin SSL) +# * "webproxy" (Proxy Root CA) +# * "wwan_ca" (Wi-Fi WAN CA) +# * "wwan_client" (Wi-Fi WAN Client) +# PEPLINK_Scheme - defaults to "https" +# PEPLINK_Port - defaults to "443" +# +#returns 0 means success, otherwise error. + +######## Public functions ##################### + +_peplink_get_cookie_data() { + grep -i "\W$1=" | grep -i "^Set-Cookie:" | _tail_n 1 | _egrep_o "$1=[^;]*;" | tr -d ';' +} + +#domain keyfile certfile cafile fullchain +peplink_deploy() { + + _cdomain="$1" + _ckey="$2" + _cfullchain="$5" + + _debug _cdomain "$_cdomain" + _debug _cfullchain "$_cfullchain" + _debug _ckey "$_ckey" + + # Get Hostname, Username and Password, but don't save until we successfully authenticate + _getdeployconf PEPLINK_Hostname + _getdeployconf PEPLINK_Username + _getdeployconf PEPLINK_Password + if [ -z "${PEPLINK_Hostname:-}" ] || [ -z "${PEPLINK_Username:-}" ] || [ -z "${PEPLINK_Password:-}" ]; then + _err "PEPLINK_Hostname & PEPLINK_Username & PEPLINK_Password must be set" + return 1 + fi + _debug2 PEPLINK_Hostname "$PEPLINK_Hostname" + _debug2 PEPLINK_Username "$PEPLINK_Username" + _secure_debug2 PEPLINK_Password "$PEPLINK_Password" + + # Optional certificate type, scheme, and port for Peplink + _getdeployconf PEPLINK_Certtype + _getdeployconf PEPLINK_Scheme + _getdeployconf PEPLINK_Port + + # Don't save the certificate type until we verify it exists and is supported + _savedeployconf PEPLINK_Scheme "$PEPLINK_Scheme" + _savedeployconf PEPLINK_Port "$PEPLINK_Port" + + # Default vaules for certificate type, scheme, and port + [ -n "${PEPLINK_Certtype}" ] || PEPLINK_Certtype="webadmin" + [ -n "${PEPLINK_Scheme}" ] || PEPLINK_Scheme="https" + [ -n "${PEPLINK_Port}" ] || PEPLINK_Port="443" + + _debug2 PEPLINK_Certtype "$PEPLINK_Certtype" + _debug2 PEPLINK_Scheme "$PEPLINK_Scheme" + _debug2 PEPLINK_Port "$PEPLINK_Port" + + _base_url="$PEPLINK_Scheme://$PEPLINK_Hostname:$PEPLINK_Port" + _debug _base_url "$_base_url" + + # Login, get the auth token from the cookie + _info "Logging into $PEPLINK_Hostname:$PEPLINK_Port" + encoded_username="$(printf "%s" "$PEPLINK_Username" | _url_encode)" + encoded_password="$(printf "%s" "$PEPLINK_Password" | _url_encode)" + response=$(_post "func=login&username=$encoded_username&password=$encoded_password" "$_base_url/cgi-bin/MANGA/api.cgi") + auth_token=$(_peplink_get_cookie_data "bauth" <"$HTTP_HEADER") + _debug3 response "$response" + _debug auth_token "$auth_token" + + if [ -z "$auth_token" ]; then + _err "Unable to authenticate to $PEPLINK_Hostname:$PEPLINK_Port using $PEPLINK_Scheme." + _err "Check your username and password." + return 1 + fi + + _H1="Cookie: $auth_token" + export _H1 + _debug2 H1 "${_H1}" + + # Now that we know the hostnameusername and password are good, save them + _savedeployconf PEPLINK_Hostname "$PEPLINK_Hostname" + _savedeployconf PEPLINK_Username "$PEPLINK_Username" + _savedeployconf PEPLINK_Password "$PEPLINK_Password" + + _info "Generate form POST request" + + encoded_key="$(_url_encode <"$_ckey")" + encoded_fullchain="$(_url_encode <"$_cfullchain")" + body="cert_type=$PEPLINK_Certtype&cert_uid=§ion=CERT_modify&key_pem=$encoded_key&key_pem_passphrase=&key_pem_passphrase_confirm=&cert_pem=$encoded_fullchain" + _debug3 body "$body" + + _info "Upload $PEPLINK_Certtype certificate to the Peplink" + + response=$(_post "$body" "$_base_url/cgi-bin/MANGA/admin.cgi") + _debug3 response "$response" + + if echo "$response" | grep 'Success' >/dev/null; then + # We've verified this certificate type is valid, so save it + _savedeployconf PEPLINK_Certtype "$PEPLINK_Certtype" + _info "Certificate was updated" + return 0 + else + _err "Unable to update certificate, error code $response" + return 1 + fi +} diff --git a/deploy/proxmoxve.sh b/deploy/proxmoxve.sh new file mode 100644 index 00000000..f9de590c --- /dev/null +++ b/deploy/proxmoxve.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env sh + +# Deploy certificates to a proxmox virtual environment node using the API. +# +# Environment variables that can be set are: +# `DEPLOY_PROXMOXVE_SERVER`: The hostname of the proxmox ve node. Defaults to +# _cdomain. +# `DEPLOY_PROXMOXVE_SERVER_PORT`: The port number the management interface is on. +# Defaults to 8006. +# `DEPLOY_PROXMOXVE_NODE_NAME`: The name of the node we'll be connecting to. +# Defaults to the host portion of the server +# domain name. +# `DEPLOY_PROXMOXVE_USER`: The user we'll connect as. Defaults to root. +# `DEPLOY_PROXMOXVE_USER_REALM`: The authentication realm the user authenticates +# with. Defaults to pam. +# `DEPLOY_PROXMOXVE_API_TOKEN_NAME`: The name of the API token created for the +# user account. Defaults to acme. +# `DEPLOY_PROXMOXVE_API_TOKEN_KEY`: The API token. Required. + +proxmoxve_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + + _debug _cdomain "$_cdomain" + _debug2 _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + # "Sane" defaults. + _getdeployconf DEPLOY_PROXMOXVE_SERVER + if [ -z "$DEPLOY_PROXMOXVE_SERVER" ]; then + _target_hostname="$_cdomain" + else + _target_hostname="$DEPLOY_PROXMOXVE_SERVER" + _savedeployconf DEPLOY_PROXMOXVE_SERVER "$DEPLOY_PROXMOXVE_SERVER" + fi + _debug2 DEPLOY_PROXMOXVE_SERVER "$_target_hostname" + + _getdeployconf DEPLOY_PROXMOXVE_SERVER_PORT + if [ -z "$DEPLOY_PROXMOXVE_SERVER_PORT" ]; then + _target_port="8006" + else + _target_port="$DEPLOY_PROXMOXVE_SERVER_PORT" + _savedeployconf DEPLOY_PROXMOXVE_SERVER_PORT "$DEPLOY_PROXMOXVE_SERVER_PORT" + fi + _debug2 DEPLOY_PROXMOXVE_SERVER_PORT "$_target_port" + + _getdeployconf DEPLOY_PROXMOXVE_NODE_NAME + if [ -z "$DEPLOY_PROXMOXVE_NODE_NAME" ]; then + _node_name=$(echo "$_target_hostname" | cut -d. -f1) + else + _node_name="$DEPLOY_PROXMOXVE_NODE_NAME" + _savedeployconf DEPLOY_PROXMOXVE_NODE_NAME "$DEPLOY_PROXMOXVE_NODE_NAME" + fi + _debug2 DEPLOY_PROXMOXVE_NODE_NAME "$_node_name" + + # Complete URL. + _target_url="https://${_target_hostname}:${_target_port}/api2/json/nodes/${_node_name}/certificates/custom" + _debug TARGET_URL "$_target_url" + + # More "sane" defaults. + _getdeployconf DEPLOY_PROXMOXVE_USER + if [ -z "$DEPLOY_PROXMOXVE_USER" ]; then + _proxmoxve_user="root" + else + _proxmoxve_user="$DEPLOY_PROXMOXVE_USER" + _savedeployconf DEPLOY_PROXMOXVE_USER "$DEPLOY_PROXMOXVE_USER" + fi + _debug2 DEPLOY_PROXMOXVE_USER "$_proxmoxve_user" + + _getdeployconf DEPLOY_PROXMOXVE_USER_REALM + if [ -z "$DEPLOY_PROXMOXVE_USER_REALM" ]; then + _proxmoxve_user_realm="pam" + else + _proxmoxve_user_realm="$DEPLOY_PROXMOXVE_USER_REALM" + _savedeployconf DEPLOY_PROXMOXVE_USER_REALM "$DEPLOY_PROXMOXVE_USER_REALM" + fi + _debug2 DEPLOY_PROXMOXVE_USER_REALM "$_proxmoxve_user_realm" + + _getdeployconf DEPLOY_PROXMOXVE_API_TOKEN_NAME + if [ -z "$DEPLOY_PROXMOXVE_API_TOKEN_NAME" ]; then + _proxmoxve_api_token_name="acme" + else + _proxmoxve_api_token_name="$DEPLOY_PROXMOXVE_API_TOKEN_NAME" + _savedeployconf DEPLOY_PROXMOXVE_API_TOKEN_NAME "$DEPLOY_PROXMOXVE_API_TOKEN_NAME" + fi + _debug2 DEPLOY_PROXMOXVE_API_TOKEN_NAME "$_proxmoxve_api_token_name" + + # This is required. + _getdeployconf DEPLOY_PROXMOXVE_API_TOKEN_KEY + if [ -z "$DEPLOY_PROXMOXVE_API_TOKEN_KEY" ]; then + _err "API key not provided." + return 1 + else + _proxmoxve_api_token_key="$DEPLOY_PROXMOXVE_API_TOKEN_KEY" + _savedeployconf DEPLOY_PROXMOXVE_API_TOKEN_KEY "$DEPLOY_PROXMOXVE_API_TOKEN_KEY" + fi + _debug2 DEPLOY_PROXMOXVE_API_TOKEN_KEY "$_proxmoxve_api_token_key" + + # PVE API Token header value. Used in "Authorization: PVEAPIToken". + _proxmoxve_header_api_token="${_proxmoxve_user}@${_proxmoxve_user_realm}!${_proxmoxve_api_token_name}=${_proxmoxve_api_token_key}" + _debug2 "Auth Header" "$_proxmoxve_header_api_token" + + # Ugly. I hate putting heredocs inside functions because heredocs don't + # account for whitespace correctly but it _does_ work and is several times + # cleaner than anything else I had here. + # + # This dumps the json payload to a variable that should be passable to the + # _psot function. + _json_payload=$( + cat < 6.41.3, but it is not guaranteed that it will be +# true for future versions when upgrading. +# +# If the router have other certificates with the same name as the one +# beeing deployed, then this script will remove those certificates. +# +# At the end of the script, the services that use those certificates +# could be updated. Currently only the www-ssl service is beeing +# updated, but more services could be added. +# +# For instance: +# ```sh +# export ROUTER_OS_ADDITIONAL_SERVICES="/ip service set api-ssl certificate=$_cdomain.cer_0" +# ``` +# +# One optional thing to do as well is to create a script that updates +# all the required services and run that script in a single command. +# +# To adopt parameters to `scp` and/or `ssh` set the optional +# `ROUTER_OS_SSH_CMD` and `ROUTER_OS_SCP_CMD` variables accordingly, +# see ssh(1) and scp(1) for parameters to those commands. +# +# Example: +# ```ssh +# export ROUTER_OS_SSH_CMD="ssh -i /acme.sh/.ssh/router.example.com -o UserKnownHostsFile=/acme.sh/.ssh/known_hosts" +# export ROUTER_OS_SCP_CMD="scp -i /acme.sh/.ssh/router.example.com -o UserKnownHostsFile=/acme.sh/.ssh/known_hosts" +# ```` +# +# returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +routeros_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + _err_code=0 + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + _getdeployconf ROUTER_OS_HOST + + if [ -z "$ROUTER_OS_HOST" ]; then + _debug "Using _cdomain as ROUTER_OS_HOST, please set if not correct." + ROUTER_OS_HOST="$_cdomain" + fi + + _getdeployconf ROUTER_OS_USERNAME + + if [ -z "$ROUTER_OS_USERNAME" ]; then + _err "Need to set the env variable ROUTER_OS_USERNAME" + return 1 + fi + + _getdeployconf ROUTER_OS_PORT + + if [ -z "$ROUTER_OS_PORT" ]; then + _debug "Using default port 22 as ROUTER_OS_PORT, please set if not correct." + ROUTER_OS_PORT=22 + fi + + _getdeployconf ROUTER_OS_SSH_CMD + + if [ -z "$ROUTER_OS_SSH_CMD" ]; then + _debug "Use default ssh setup." + ROUTER_OS_SSH_CMD="ssh -p $ROUTER_OS_PORT" + fi + + _getdeployconf ROUTER_OS_SCP_CMD + + if [ -z "$ROUTER_OS_SCP_CMD" ]; then + _debug "USe default scp setup." + ROUTER_OS_SCP_CMD="scp -P $ROUTER_OS_PORT" + fi + + _getdeployconf ROUTER_OS_ADDITIONAL_SERVICES + + if [ -z "$ROUTER_OS_ADDITIONAL_SERVICES" ]; then + _debug "Not enabling additional services" + ROUTER_OS_ADDITIONAL_SERVICES="" + fi + + _savedeployconf ROUTER_OS_HOST "$ROUTER_OS_HOST" + _savedeployconf ROUTER_OS_USERNAME "$ROUTER_OS_USERNAME" + _savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT" + _savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD" + _savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD" + _savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES" + + # push key to routeros + if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then + return $_err_code + fi + + # push certificate chain to routeros + if ! _scp_certificate "$_cfullchain" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.cer"; then + return $_err_code + fi + + DEPLOY_SCRIPT_CMD="/system script add name=\"LECertDeploy-$_cdomain\" owner=$ROUTER_OS_USERNAME \ +comment=\"generated by routeros deploy script in acme.sh\" \ +source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\ +\n/certificate remove [ find name=$_cdomain.cer_1 ];\ +\n/certificate remove [ find name=$_cdomain.cer_2 ];\ +\ndelay 1;\ +\n/certificate import file-name=$_cdomain.cer passphrase=\\\"\\\";\ +\n/certificate import file-name=$_cdomain.key passphrase=\\\"\\\";\ +\ndelay 1;\ +\n/file remove $_cdomain.cer;\ +\n/file remove $_cdomain.key;\ +\ndelay 2;\ +\n/ip service set www-ssl certificate=$_cdomain.cer_0;\ +\n$ROUTER_OS_ADDITIONAL_SERVICES;\ +\n\" +" + + if ! _ssh_remote_cmd "$DEPLOY_SCRIPT_CMD"; then + return $_err_code + fi + + if ! _ssh_remote_cmd "/system script run \"LECertDeploy-$_cdomain\""; then + return $_err_code + fi + + if ! _ssh_remote_cmd "/system script remove \"LECertDeploy-$_cdomain\""; then + return $_err_code + fi + + return 0 +} + +# inspired by deploy/ssh.sh +_ssh_remote_cmd() { + _cmd="$1" + _secure_debug "Remote commands to execute: $_cmd" + _info "Submitting sequence of commands to routeros" + # quotations in bash cmd below intended. Squash travis spellcheck error + # shellcheck disable=SC2029 + $ROUTER_OS_SSH_CMD "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST" "$_cmd" + _err_code="$?" + + if [ "$_err_code" != "0" ]; then + _err "Error code $_err_code returned from routeros" + fi + + return $_err_code +} + +_scp_certificate() { + _src="$1" + _dst="$2" + _secure_debug "scp '$_src' to '$_dst'" + _info "Push key '$_src' to routeros" + + $ROUTER_OS_SCP_CMD "$_src" "$_dst" + _err_code="$?" + + if [ "$_err_code" != "0" ]; then + _err "Error code $_err_code returned from scp" + fi + + return $_err_code +} diff --git a/deploy/ssh.sh b/deploy/ssh.sh index 9cb0af9e..c66e2e19 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -12,15 +12,19 @@ # Only a username is required. All others are optional. # # The following examples are for QNAP NAS running QTS 4.2 -# export DEPLOY_SSH_CMD="" # defaults to ssh +# export DEPLOY_SSH_CMD="" # defaults to "ssh -T" # export DEPLOY_SSH_USER="admin" # required -# export DEPLOY_SSH_SERVER="qnap" # defaults to domain name +# export DEPLOY_SSH_SERVER="host1 host2:8022 192.168.0.1:9022" # defaults to domain name, support multiple servers with optional port # export DEPLOY_SSH_KEYFILE="/etc/stunnel/stunnel.pem" # export DEPLOY_SSH_CERTFILE="/etc/stunnel/stunnel.pem" # export DEPLOY_SSH_CAFILE="/etc/stunnel/uca.pem" # export DEPLOY_SSH_FULLCHAIN="" # export DEPLOY_SSH_REMOTE_CMD="/etc/init.d/stunnel.sh restart" -# export DEPLOY_SSH_BACKUP="" # yes or no, default to yes +# export DEPLOY_SSH_BACKUP="" # yes or no, default to yes or previously saved value +# export DEPLOY_SSH_BACKUP_PATH=".acme_ssh_deploy" # path on remote system. Defaults to .acme_ssh_deploy +# export DEPLOY_SSH_MULTI_CALL="" # yes or no, default to no or previously saved value +# export DEPLOY_SSH_USE_SCP="" yes or no, default to no +# export DEPLOY_SSH_SCP_CMD="" defaults to "scp -q" # ######## Public functions ##################### @@ -31,15 +35,7 @@ ssh_deploy() { _ccert="$3" _cca="$4" _cfullchain="$5" - _cmdstr="" - _homedir='~' - _backupprefix="$_homedir/.acme_ssh_deploy/$_cdomain-backup" - _backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')" - - if [ -f "$DOMAIN_CONF" ]; then - # shellcheck disable=SC1090 - . "$DOMAIN_CONF" - fi + _deploy_ssh_servers="" _debug _cdomain "$_cdomain" _debug _ckey "$_ckey" @@ -48,136 +44,163 @@ ssh_deploy() { _debug _cfullchain "$_cfullchain" # USER is required to login by SSH to remote host. + _migratedeployconf Le_Deploy_ssh_user DEPLOY_SSH_USER + _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." - return 1 - fi - else - Le_Deploy_ssh_user="$DEPLOY_SSH_USER" - _savedomainconf Le_Deploy_ssh_user "$Le_Deploy_ssh_user" + _err "DEPLOY_SSH_USER not defined." + return 1 fi + _savedeployconf DEPLOY_SSH_USER "$DEPLOY_SSH_USER" # SERVER is optional. If not provided then use _cdomain - if [ -n "$DEPLOY_SSH_SERVER" ]; then - Le_Deploy_ssh_server="$DEPLOY_SSH_SERVER" - _savedomainconf Le_Deploy_ssh_server "$Le_Deploy_ssh_server" - elif [ -z "$Le_Deploy_ssh_server" ]; then - Le_Deploy_ssh_server="$_cdomain" + _migratedeployconf Le_Deploy_ssh_server DEPLOY_SSH_SERVER + _getdeployconf DEPLOY_SSH_SERVER + _debug2 DEPLOY_SSH_SERVER "$DEPLOY_SSH_SERVER" + if [ -z "$DEPLOY_SSH_SERVER" ]; then + DEPLOY_SSH_SERVER="$_cdomain" fi + _savedeployconf DEPLOY_SSH_SERVER "$DEPLOY_SSH_SERVER" # CMD is optional. If not provided then use ssh - if [ -n "$DEPLOY_SSH_CMD" ]; then - Le_Deploy_ssh_cmd="$DEPLOY_SSH_CMD" - _savedomainconf Le_Deploy_ssh_cmd "$Le_Deploy_ssh_cmd" - elif [ -z "$Le_Deploy_ssh_cmd" ]; then - Le_Deploy_ssh_cmd="ssh" + _migratedeployconf Le_Deploy_ssh_cmd DEPLOY_SSH_CMD + _getdeployconf DEPLOY_SSH_CMD + _debug2 DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD" + if [ -z "$DEPLOY_SSH_CMD" ]; then + DEPLOY_SSH_CMD="ssh -T" fi + _savedeployconf DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD" - # BACKUP is optional. If not provided then default to yes - if [ "$DEPLOY_SSH_BACKUP" = "no" ]; then - Le_Deploy_ssh_backup="no" - elif [ -z "$Le_Deploy_ssh_backup" ]; then - Le_Deploy_ssh_backup="yes" + # BACKUP is optional. If not provided then default to previously saved value or yes. + _migratedeployconf Le_Deploy_ssh_backup DEPLOY_SSH_BACKUP + _getdeployconf DEPLOY_SSH_BACKUP + _debug2 DEPLOY_SSH_BACKUP "$DEPLOY_SSH_BACKUP" + if [ -z "$DEPLOY_SSH_BACKUP" ]; then + DEPLOY_SSH_BACKUP="yes" fi - _savedomainconf Le_Deploy_ssh_backup "$Le_Deploy_ssh_backup" + _savedeployconf DEPLOY_SSH_BACKUP "$DEPLOY_SSH_BACKUP" - _info "Deploy certificates to remote server $Le_Deploy_ssh_user@$Le_Deploy_ssh_server" + # BACKUP_PATH is optional. If not provided then default to previously saved value or .acme_ssh_deploy + _migratedeployconf Le_Deploy_ssh_backup_path DEPLOY_SSH_BACKUP_PATH + _getdeployconf DEPLOY_SSH_BACKUP_PATH + _debug2 DEPLOY_SSH_BACKUP_PATH "$DEPLOY_SSH_BACKUP_PATH" + if [ -z "$DEPLOY_SSH_BACKUP_PATH" ]; then + DEPLOY_SSH_BACKUP_PATH=".acme_ssh_deploy" + fi + _savedeployconf DEPLOY_SSH_BACKUP_PATH "$DEPLOY_SSH_BACKUP_PATH" + + # MULTI_CALL is optional. If not provided then default to previously saved + # value (which may be undefined... equivalent to "no"). + _migratedeployconf Le_Deploy_ssh_multi_call DEPLOY_SSH_MULTI_CALL + _getdeployconf DEPLOY_SSH_MULTI_CALL + _debug2 DEPLOY_SSH_MULTI_CALL "$DEPLOY_SSH_MULTI_CALL" + if [ -z "$DEPLOY_SSH_MULTI_CALL" ]; then + DEPLOY_SSH_MULTI_CALL="no" + fi + _savedeployconf DEPLOY_SSH_MULTI_CALL "$DEPLOY_SSH_MULTI_CALL" # KEYFILE is optional. # If provided then private key will be copied to provided filename. + _migratedeployconf Le_Deploy_ssh_keyfile DEPLOY_SSH_KEYFILE + _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" - fi - if [ -n "$Le_Deploy_ssh_keyfile" ]; then - if [ "$Le_Deploy_ssh_backup" = "yes" ]; then - # backup file we are about to overwrite. - _cmdstr="$_cmdstr cp $Le_Deploy_ssh_keyfile $_backupdir >/dev/null;" - fi - # copy new certificate into file. - _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $Le_Deploy_ssh_keyfile;" - _info "will copy private key to remote file $Le_Deploy_ssh_keyfile" + _savedeployconf DEPLOY_SSH_KEYFILE "$DEPLOY_SSH_KEYFILE" fi # CERTFILE is optional. # If provided then certificate will be copied or appended to provided filename. + _migratedeployconf Le_Deploy_ssh_certfile DEPLOY_SSH_CERTFILE + _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" - fi - if [ -n "$Le_Deploy_ssh_certfile" ]; then - _pipe=">" - if [ "$Le_Deploy_ssh_certfile" = "$Le_Deploy_ssh_keyfile" ]; then - # if filename is same as previous file then append. - _pipe=">>" - elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then - # backup file we are about to overwrite. - _cmdstr="$_cmdstr cp $Le_Deploy_ssh_certfile $_backupdir >/dev/null;" - fi - # copy new certificate into file. - _cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $Le_Deploy_ssh_certfile;" - _info "will copy certificate to remote file $Le_Deploy_ssh_certfile" + _savedeployconf DEPLOY_SSH_CERTFILE "$DEPLOY_SSH_CERTFILE" fi # CAFILE is optional. # If provided then CA intermediate certificate will be copied or appended to provided filename. + _migratedeployconf Le_Deploy_ssh_cafile DEPLOY_SSH_CAFILE + _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" - fi - if [ -n "$Le_Deploy_ssh_cafile" ]; then - _pipe=">" - if [ "$Le_Deploy_ssh_cafile" = "$Le_Deploy_ssh_keyfile" ] \ - || [ "$Le_Deploy_ssh_cafile" = "$Le_Deploy_ssh_certfile" ]; then - # if filename is same as previous file then append. - _pipe=">>" - elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then - # backup file we are about to overwrite. - _cmdstr="$_cmdstr cp $Le_Deploy_ssh_cafile $_backupdir >/dev/null;" - fi - # copy new certificate into file. - _cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $Le_Deploy_ssh_cafile;" - _info "will copy CA file to remote file $Le_Deploy_ssh_cafile" + _savedeployconf DEPLOY_SSH_CAFILE "$DEPLOY_SSH_CAFILE" fi # FULLCHAIN is optional. # If provided then fullchain certificate will be copied or appended to provided filename. + _migratedeployconf Le_Deploy_ssh_fullchain DEPLOY_SSH_FULLCHAIN + _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" - fi - if [ -n "$Le_Deploy_ssh_fullchain" ]; then - _pipe=">" - if [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_keyfile" ] \ - || [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_certfile" ] \ - || [ "$Le_Deploy_ssh_fullchain" = "$Le_Deploy_ssh_cafile" ]; then - # if filename is same as previous file then append. - _pipe=">>" - elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then - # backup file we are about to overwrite. - _cmdstr="$_cmdstr cp $Le_Deploy_ssh_fullchain $_backupdir >/dev/null;" - fi - # copy new certificate into file. - _cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $Le_Deploy_ssh_fullchain;" - _info "will copy fullchain to remote file $Le_Deploy_ssh_fullchain" + _savedeployconf DEPLOY_SSH_FULLCHAIN "$DEPLOY_SSH_FULLCHAIN" fi # REMOTE_CMD is optional. # If provided then this command will be executed on remote host. + _migratedeployconf Le_Deploy_ssh_remote_cmd DEPLOY_SSH_REMOTE_CMD + _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" - fi - if [ -n "$Le_Deploy_ssh_remote_cmd" ]; then - _cmdstr="$_cmdstr $Le_Deploy_ssh_remote_cmd;" - _info "Will execute remote command $Le_Deploy_ssh_remote_cmd" + _savedeployconf DEPLOY_SSH_REMOTE_CMD "$DEPLOY_SSH_REMOTE_CMD" fi - if [ -z "$_cmdstr" ]; then - _err "No remote commands to excute. Failed to deploy certificates to remote server" - return 1 - elif [ "$Le_Deploy_ssh_backup" = "yes" ]; then + # USE_SCP is optional. If not provided then default to previously saved + # value (which may be undefined... equivalent to "no"). + _getdeployconf DEPLOY_SSH_USE_SCP + _debug2 DEPLOY_SSH_USE_SCP "$DEPLOY_SSH_USE_SCP" + if [ -z "$DEPLOY_SSH_USE_SCP" ]; then + DEPLOY_SSH_USE_SCP="no" + fi + _savedeployconf DEPLOY_SSH_USE_SCP "$DEPLOY_SSH_USE_SCP" + + # SCP_CMD is optional. If not provided then use scp + _getdeployconf DEPLOY_SSH_SCP_CMD + _debug2 DEPLOY_SSH_SCP_CMD "$DEPLOY_SSH_SCP_CMD" + if [ -z "$DEPLOY_SSH_SCP_CMD" ]; then + DEPLOY_SSH_SCP_CMD="scp -q" + fi + _savedeployconf DEPLOY_SSH_SCP_CMD "$DEPLOY_SSH_SCP_CMD" + + if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then + DEPLOY_SSH_MULTI_CALL="yes" + _info "Using scp as alternate method for copying files. Multicall Mode is implicit" + elif [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + _info "Using MULTI_CALL mode... Required commands sent in multiple calls to remote host" + else + _info "Required commands batched and sent in single call to remote host" + fi + + _deploy_ssh_servers="$DEPLOY_SSH_SERVER" + for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do + _ssh_deploy + done +} + +_ssh_deploy() { + _err_code=0 + _cmdstr="" + _backupprefix="" + _backupdir="" + _local_cert_file="" + _local_ca_file="" + _local_full_file="" + + case $DEPLOY_SSH_SERVER in + *:*) + _host=${DEPLOY_SSH_SERVER%:*} + _port=${DEPLOY_SSH_SERVER##*:} + ;; + *) + _host=$DEPLOY_SSH_SERVER + _port= + ;; + esac + + _info "Deploy certificates to remote server $DEPLOY_SSH_USER@$_host:$_port" + + if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then + _backupprefix="$DEPLOY_SSH_BACKUP_PATH/$_cdomain-backup" + _backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')" # run cleanup on the backup directory, erase all older # than 180 days (15552000 seconds). _cmdstr="{ now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \ @@ -188,18 +211,252 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d _cmdstr="mkdir -p $_backupdir; $_cmdstr" _info "Backup of old certificate files will be placed in remote directory $_backupdir" _info "Backup directories erased after 180 days." + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi fi - _secure_debug "Remote commands to execute: " "$_cmdstr" - _info "Submitting sequence of commands to remote server by ssh" + if [ -n "$DEPLOY_SSH_KEYFILE" ]; then + if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then + # backup file we are about to overwrite. + _cmdstr="$_cmdstr cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null;" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi + fi + + # copy new key into file. + if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then + # scp the file + if ! _scp_remote_cmd "$_ckey" "$DEPLOY_SSH_KEYFILE"; then + return $_err_code + fi + else + # ssh echo to the file + _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;" + _info "will copy private key to remote file $DEPLOY_SSH_KEYFILE" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi + fi + fi + + if [ -n "$DEPLOY_SSH_CERTFILE" ]; then + _pipe=">" + if [ "$DEPLOY_SSH_CERTFILE" = "$DEPLOY_SSH_KEYFILE" ]; then + # if filename is same as previous file then append. + _pipe=">>" + elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then + # backup file we are about to overwrite. + _cmdstr="$_cmdstr cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null;" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi + fi + + # copy new certificate into file. + if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then + # scp the file + _local_cert_file=$(_mktemp) + if [ "$DEPLOY_SSH_CERTFILE" = "$DEPLOY_SSH_KEYFILE" ]; then + cat "$_ckey" >>"$_local_cert_file" + fi + cat "$_ccert" >>"$_local_cert_file" + if ! _scp_remote_cmd "$_local_cert_file" "$DEPLOY_SSH_CERTFILE"; then + return $_err_code + fi + else + # ssh echo to the file + _cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE;" + _info "will copy certificate to remote file $DEPLOY_SSH_CERTFILE" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi + fi + fi + + if [ -n "$DEPLOY_SSH_CAFILE" ]; then + _pipe=">" + if [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_KEYFILE" ] || + [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_CERTFILE" ]; then + # if filename is same as previous file then append. + _pipe=">>" + elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then + # backup file we are about to overwrite. + _cmdstr="$_cmdstr cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null;" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi + fi + + # copy new certificate into file. + if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then + # scp the file + _local_ca_file=$(_mktemp) + if [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_KEYFILE" ]; then + cat "$_ckey" >>"$_local_ca_file" + fi + if [ "$DEPLOY_SSH_CAFILE" = "$DEPLOY_SSH_CERTFILE" ]; then + cat "$_ccert" >>"$_local_ca_file" + fi + cat "$_cca" >>"$_local_ca_file" + if ! _scp_remote_cmd "$_local_ca_file" "$DEPLOY_SSH_CAFILE"; then + return $_err_code + fi + else + # ssh echo to the file + _cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE;" + _info "will copy CA file to remote file $DEPLOY_SSH_CAFILE" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi + fi + fi + + if [ -n "$DEPLOY_SSH_FULLCHAIN" ]; then + _pipe=">" + if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_KEYFILE" ] || + [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CERTFILE" ] || + [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CAFILE" ]; then + # if filename is same as previous file then append. + _pipe=">>" + elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then + # backup file we are about to overwrite. + _cmdstr="$_cmdstr cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null;" + if [ "$DEPLOY_SSH_FULLCHAIN" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi + fi + + # copy new certificate into file. + if [ "$DEPLOY_SSH_USE_SCP" = "yes" ]; then + # scp the file + _local_full_file=$(_mktemp) + if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_KEYFILE" ]; then + cat "$_ckey" >>"$_local_full_file" + fi + if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CERTFILE" ]; then + cat "$_ccert" >>"$_local_full_file" + fi + if [ "$DEPLOY_SSH_FULLCHAIN" = "$DEPLOY_SSH_CAFILE" ]; then + cat "$_cca" >>"$_local_full_file" + fi + cat "$_cfullchain" >>"$_local_full_file" + if ! _scp_remote_cmd "$_local_full_file" "$DEPLOY_SSH_FULLCHAIN"; then + return $_err_code + fi + else + # ssh echo to the file + _cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN;" + _info "will copy fullchain to remote file $DEPLOY_SSH_FULLCHAIN" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi + fi + fi + + # cleanup local files if any + if [ -f "$_local_cert_file" ]; then + rm -f "$_local_cert_file" + fi + if [ -f "$_local_ca_file" ]; then + rm -f "$_local_ca_file" + fi + if [ -f "$_local_full_file" ]; then + rm -f "$_local_full_file" + fi + + if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then + _cmdstr="$_cmdstr $DEPLOY_SSH_REMOTE_CMD;" + _info "Will execute remote command $DEPLOY_SSH_REMOTE_CMD" + if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + _cmdstr="" + fi + fi + + # if commands not all sent in multiple calls then all commands sent in a single SSH call now... + if [ -n "$_cmdstr" ]; then + if ! _ssh_remote_cmd "$_cmdstr"; then + return $_err_code + fi + fi + # cleanup in case all is ok + return 0 +} + +#cmd +_ssh_remote_cmd() { + _cmd="$1" + + _ssh_cmd="$DEPLOY_SSH_CMD" + if [ -n "$_port" ]; then + _ssh_cmd="$_ssh_cmd -p $_port" + fi + + _secure_debug "Remote commands to execute: $_cmd" + _info "Submitting sequence of commands to remote server by $_ssh_cmd" + # quotations in bash cmd below intended. Squash travis spellcheck error # shellcheck disable=SC2029 - $Le_Deploy_ssh_cmd -T "$Le_Deploy_ssh_user@$Le_Deploy_ssh_server" sh -c "'$_cmdstr'" - _ret="$?" + $_ssh_cmd "$DEPLOY_SSH_USER@$_host" sh -c "'$_cmd'" + _err_code="$?" - if [ "$_ret" != "0" ]; then - _err "Error code $_ret returned from $Le_Deploy_ssh_cmd" + if [ "$_err_code" != "0" ]; then + _err "Error code $_err_code returned from ssh" fi - return $_ret + return $_err_code +} + +# cmd scp +_scp_remote_cmd() { + _src=$1 + _dest=$2 + + _scp_cmd="$DEPLOY_SSH_SCP_CMD" + if [ -n "$_port" ]; then + _scp_cmd="$_scp_cmd -P $_port" + fi + + _secure_debug "Remote copy source $_src to destination $_dest" + _info "Submitting secure copy by $_scp_cmd" + + $_scp_cmd "$_src" "$DEPLOY_SSH_USER"@"$_host":"$_dest" + _err_code="$?" + + if [ "$_err_code" != "0" ]; then + _err "Error code $_err_code returned from scp" + fi + + return $_err_code } diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh new file mode 100644 index 00000000..1d945cad --- /dev/null +++ b/deploy/synology_dsm.sh @@ -0,0 +1,229 @@ +#!/bin/bash + +################################################################################ +# ACME.sh 3rd party deploy plugin for Synology DSM +################################################################################ +# Authors: Brian Hartvigsen (creator), https://github.com/tresni +# Martin Arndt (contributor), https://troublezone.net/ +# Updated: 2023-07-03 +# Issues: https://github.com/acmesh-official/acme.sh/issues/2727 +################################################################################ +# Usage: +# 1. export SYNO_Username="adminUser" +# 2. export SYNO_Password="adminPassword" +# Optional exports (shown values are the defaults): +# - export SYNO_Certificate="" to replace a specific certificate via description +# - export SYNO_Scheme="http" +# - export SYNO_Hostname="localhost" +# - export SYNO_Port="5000" +# - export SYNO_Device_Name="CertRenewal" - required for skipping 2FA-OTP +# - export SYNO_Device_ID="" - required for skipping 2FA-OTP +# 3. acme.sh --deploy --deploy-hook synology_dsm -d example.com +################################################################################ +# Dependencies: +# - jq & curl +################################################################################ +# Return value: +# 0 means success, otherwise error. +################################################################################ + +########## Public functions #################################################### +#domain keyfile certfile cafile fullchain +synology_dsm_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + + _debug _cdomain "$_cdomain" + + # Get username & password, but don't save until we authenticated successfully + _getdeployconf SYNO_Username + _getdeployconf SYNO_Password + _getdeployconf SYNO_Create + _getdeployconf SYNO_DID + _getdeployconf SYNO_TOTP_SECRET + _getdeployconf SYNO_Device_Name + _getdeployconf SYNO_Device_ID + if [ -z "${SYNO_Username:-}" ] || [ -z "${SYNO_Password:-}" ]; then + _err "SYNO_Username & SYNO_Password must be set" + return 1 + fi + if [ -n "${SYNO_Device_Name:-}" ] && [ -z "${SYNO_Device_ID:-}" ]; then + _err "SYNO_Device_Name set, but SYNO_Device_ID is empty" + return 1 + fi + _debug2 SYNO_Username "$SYNO_Username" + _secure_debug2 SYNO_Password "$SYNO_Password" + _debug2 SYNO_Create "$SYNO_Create" + _debug2 SYNO_Device_Name "$SYNO_Device_Name" + _secure_debug2 SYNO_Device_ID "$SYNO_Device_ID" + + # Optional scheme, hostname & port for Synology DSM + _getdeployconf SYNO_Scheme + _getdeployconf SYNO_Hostname + _getdeployconf SYNO_Port + + # Default values for scheme, hostname & port + # Defaulting to localhost & http, because it's localhost… + [ -n "${SYNO_Scheme}" ] || SYNO_Scheme="http" + [ -n "${SYNO_Hostname}" ] || SYNO_Hostname="localhost" + [ -n "${SYNO_Port}" ] || SYNO_Port="5000" + _savedeployconf SYNO_Scheme "$SYNO_Scheme" + _savedeployconf SYNO_Hostname "$SYNO_Hostname" + _savedeployconf SYNO_Port "$SYNO_Port" + _debug2 SYNO_Scheme "$SYNO_Scheme" + _debug2 SYNO_Hostname "$SYNO_Hostname" + _debug2 SYNO_Port "$SYNO_Port" + + # Get the certificate description, but don't save it until we verify it's real + _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" + + _debug "Getting API version" + response=$(_get "$_base_url/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query&query=SYNO.API.Auth") + api_path=$(echo "$response" | grep "SYNO.API.Auth" | sed -n 's/.*"path" *: *"\([^"]*\)".*/\1/p') + api_version=$(echo "$response" | grep "SYNO.API.Auth" | sed -n 's/.*"maxVersion" *: *\([0-9]*\).*/\1/p') + _debug3 response "$response" + _debug3 api_path "$api_path" + _debug3 api_version "$api_version" + + # Login, get the session ID & SynoToken from JSON + _info "Logging into $SYNO_Hostname:$SYNO_Port" + encoded_username="$(printf "%s" "$SYNO_Username" | _url_encode)" + encoded_password="$(printf "%s" "$SYNO_Password" | _url_encode)" + + otp_code="" + # START - DEPRECATED, only kept for legacy compatibility reasons + if [ -n "$SYNO_TOTP_SECRET" ]; then + _info "WARNING: Usage of SYNO_TOTP_SECRET is deprecated!" + _info " See synology_dsm.sh script or ACME.sh Wiki page for details:" + _info " https://github.com/acmesh-official/acme.sh/wiki/Synology-NAS-Guide" + DEPRECATED_otp_code="" + if _exists oathtool; then + DEPRECATED_otp_code="$(oathtool --base32 --totp "${SYNO_TOTP_SECRET}" 2>/dev/null)" + else + _err "oathtool could not be found, install oathtool to use SYNO_TOTP_SECRET" + return 1 + fi + + if [ -n "$SYNO_DID" ]; then + _H1="Cookie: did=$SYNO_DID" + export _H1 + _debug3 H1 "${_H1}" + fi + + response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version&enable_syno_token=yes&otp_code=$DEPRECATED_otp_code&device_name=certrenewal&device_id=$SYNO_DID" "$_base_url/webapi/auth.cgi?enable_syno_token=yes") + _debug3 response "$response" + # END - DEPRECATED, only kept for legacy compatibility reasons + # Get device ID if still empty first, otherwise log in right away + elif [ -z "${SYNO_Device_ID:-}" ]; then + printf "Enter OTP code for user '%s': " "$SYNO_Username" + read -r otp_code + if [ -z "${SYNO_Device_Name:-}" ]; then + printf "Enter device name or leave empty for default (CertRenewal): " + read -r SYNO_Device_Name + [ -n "${SYNO_Device_Name}" ] || SYNO_Device_Name="CertRenewal" + fi + + response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&otp_code=$otp_code&enable_syno_token=yes&enable_device_token=yes&device_name=$SYNO_Device_Name") + _secure_debug3 response "$response" + + id_property='device_id' + [ "${api_version}" -gt '6' ] || id_property='did' + SYNO_Device_ID=$(echo "$response" | grep "$id_property" | sed -n 's/.*"'$id_property'" *: *"\([^"]*\).*/\1/p') + _secure_debug2 SYNO_Device_ID "$SYNO_Device_ID" + else + response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&device_name=$SYNO_Device_Name&device_id=$SYNO_Device_ID") + _debug3 response "$response" + fi + + sid=$(echo "$response" | grep "sid" | sed -n 's/.*"sid" *: *"\([^"]*\).*/\1/p') + token=$(echo "$response" | grep "synotoken" | sed -n 's/.*"synotoken" *: *"\([^"]*\).*/\1/p') + _debug "Session ID" "$sid" + _debug SynoToken "$token" + if [ -z "$SYNO_DID" ] && [ -z "$SYNO_Device_ID" ] || [ -z "$sid" ] || [ -z "$token" ]; then + _err "Unable to authenticate to $_base_url - check your username & password." + _err "If two-factor authentication is enabled for the user, set SYNO_Device_ID." + return 1 + fi + + _H1="X-SYNO-TOKEN: $token" + export _H1 + _debug2 H1 "${_H1}" + + # Now that we know the username & password are good, save them + _savedeployconf SYNO_Username "$SYNO_Username" + _savedeployconf SYNO_Password "$SYNO_Password" + _savedeployconf SYNO_Device_Name "$SYNO_Device_Name" + _savedeployconf SYNO_Device_ID "$SYNO_Device_ID" + + _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" + 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 + _err "Unable to find certificate: $SYNO_Certificate & \$SYNO_Create is not set" + return 1 + fi + + # We've verified this certificate description is a thing, so save it + _savedeployconf SYNO_Certificate "$SYNO_Certificate" "base64" + + _info "Generate form POST request" + nl="\0015\0012" + delim="--------------------------$(_utc_date | tr -d -- '-: ')" + content="--$delim${nl}Content-Disposition: form-data; name=\"key\"; filename=\"$(basename "$_ckey")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")\0012" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"cert\"; filename=\"$(basename "$_ccert")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ccert")\0012" + 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}" + 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 + + _info "Upload certificate to the Synology DSM" + response=$(_post "$content" "$_base_url/webapi/entry.cgi?api=SYNO.Core.Certificate&method=import&version=1&SynoToken=$token&_sid=$sid" "" "POST" "multipart/form-data; boundary=${delim}") + _debug3 response "$response" + + if ! echo "$response" | grep '"error":' >/dev/null; then + if echo "$response" | grep '"restart_httpd":true' >/dev/null; then + _info "Restarting HTTP services succeeded" + else + _info "Restarting HTTP services failed" + fi + + _logout + return 0 + else + _err "Unable to update certificate, error code $response" + _logout + return 1 + fi +} + +#################### Private functions below ################################## +_logout() { + # Logout CERT user only to not occupy a permanent session, e.g. in DSM's "Connected Users" widget (based on previous variables) + response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid") + _debug3 response "$response" +} diff --git a/deploy/truenas.sh b/deploy/truenas.sh new file mode 100644 index 00000000..c79e6dac --- /dev/null +++ b/deploy/truenas.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env sh + +# Here is a scipt to deploy the cert to your TrueNAS using the REST API. +# https://www.truenas.com/docs/hub/additional-topics/api/rest_api.html +# +# Written by Frank Plass github@f-plass.de +# https://github.com/danb35/deploy-freenas/blob/master/deploy_freenas.py +# Thanks to danb35 for your template! +# +# Following environment variables must be set: +# +# export DEPLOY_TRUENAS_APIKEY="HTTPS redirection is enabled" + _info "Setting DEPLOY_TRUENAS_SCHEME to 'https'" + DEPLOY_TRUENAS_SCHEME="https" + _api_url="$DEPLOY_TRUENAS_SCHEME://$DEPLOY_TRUENAS_HOSTNAME/api/v2.0" + _savedeployconf DEPLOY_TRUENAS_SCHEME "$DEPLOY_TRUENAS_SCHEME" + fi + + _info "Uploading new certificate to TrueNAS" + _certname="Letsencrypt_$(_utc_date | tr ' ' '_' | tr -d -- ':')" + _debug3 _certname "$_certname" + + _certData="{\"create_type\": \"CERTIFICATE_CREATE_IMPORTED\", \"name\": \"${_certname}\", \"certificate\": \"$(_json_encode <"$_cfullchain")\", \"privatekey\": \"$(_json_encode <"$_ckey")\"}" + _add_cert_result="$(_post "$_certData" "$_api_url/certificate" "" "POST" "application/json")" + + _debug3 _add_cert_result "$_add_cert_result" + + _info "Fetching list of installed certificates" + _cert_list=$(_get "$_api_url/system/general/ui_certificate_choices") + _cert_id=$(echo "$_cert_list" | grep "$_certname" | sed -n 's/.*"\([0-9]\{1,\}\)".*$/\1/p') + + _debug3 _cert_id "$_cert_id" + + _info "Current activate certificate ID: $_cert_id" + _activateData="{\"ui_certificate\": \"${_cert_id}\"}" + _activate_result="$(_post "$_activateData" "$_api_url/system/general" "" "PUT" "application/json")" + + _debug3 _activate_result "$_activate_result" + + _info "Checking if WebDAV certificate is the same as the TrueNAS web UI" + _webdav_list=$(_get "$_api_url/webdav") + _webdav_cert_id=$(echo "$_webdav_list" | grep '"certssl":' | tr -d -- '"certsl: ,') + + if [ "$_webdav_cert_id" = "$_active_cert_id" ]; then + _info "Updating the WebDAV certificate" + _debug _webdav_cert_id "$_webdav_cert_id" + _webdav_data="{\"certssl\": \"${_cert_id}\"}" + _activate_webdav_cert="$(_post "$_webdav_data" "$_api_url/webdav" "" "PUT" "application/json")" + _webdav_new_cert_id=$(echo "$_activate_webdav_cert" | _json_decode | grep '"certssl":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p') + if [ "$_webdav_new_cert_id" -eq "$_cert_id" ]; then + _info "WebDAV certificate updated successfully" + else + _err "Unable to set WebDAV certificate" + _debug3 _activate_webdav_cert "$_activate_webdav_cert" + _debug3 _webdav_new_cert_id "$_webdav_new_cert_id" + return 1 + fi + _debug3 _webdav_new_cert_id "$_webdav_new_cert_id" + else + _info "WebDAV certificate is not configured or is not the same as TrueNAS web UI" + fi + + _info "Checking if FTP certificate is the same as the TrueNAS web UI" + _ftp_list=$(_get "$_api_url/ftp") + _ftp_cert_id=$(echo "$_ftp_list" | grep '"ssltls_certificate":' | tr -d -- '"certislfa:_ ,') + + if [ "$_ftp_cert_id" = "$_active_cert_id" ]; then + _info "Updating the FTP certificate" + _debug _ftp_cert_id "$_ftp_cert_id" + _ftp_data="{\"ssltls_certificate\": \"${_cert_id}\"}" + _activate_ftp_cert="$(_post "$_ftp_data" "$_api_url/ftp" "" "PUT" "application/json")" + _ftp_new_cert_id=$(echo "$_activate_ftp_cert" | _json_decode | grep '"ssltls_certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p') + if [ "$_ftp_new_cert_id" -eq "$_cert_id" ]; then + _info "FTP certificate updated successfully" + else + _err "Unable to set FTP certificate" + _debug3 _activate_ftp_cert "$_activate_ftp_cert" + _debug3 _ftp_new_cert_id "$_ftp_new_cert_id" + return 1 + fi + _debug3 _activate_ftp_cert "$_activate_ftp_cert" + else + _info "FTP certificate is not configured or is not the same as TrueNAS web UI" + fi + + _info "Checking if S3 certificate is the same as the TrueNAS web UI" + _s3_list=$(_get "$_api_url/s3") + _s3_cert_id=$(echo "$_s3_list" | grep '"certificate":' | tr -d -- '"certifa:_ ,') + + if [ "$_s3_cert_id" = "$_active_cert_id" ]; then + _info "Updating the S3 certificate" + _debug _s3_cert_id "$_s3_cert_id" + _s3_data="{\"certificate\": \"${_cert_id}\"}" + _activate_s3_cert="$(_post "$_s3_data" "$_api_url/s3" "" "PUT" "application/json")" + _s3_new_cert_id=$(echo "$_activate_s3_cert" | _json_decode | grep '"certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p') + if [ "$_s3_new_cert_id" -eq "$_cert_id" ]; then + _info "S3 certificate updated successfully" + else + _err "Unable to set S3 certificate" + _debug3 _activate_s3_cert "$_activate_s3_cert" + _debug3 _s3_new_cert_id "$_s3_new_cert_id" + return 1 + fi + _debug3 _activate_s3_cert "$_activate_s3_cert" + else + _info "S3 certificate is not configured or is not the same as TrueNAS web UI" + fi + + _info "Checking if any chart release Apps is using the same certificate as TrueNAS web UI. Tool 'jq' is required" + if _exists jq; then + _info "Query all chart release" + _release_list=$(_get "$_api_url/chart/release") + _related_name_list=$(printf "%s" "$_release_list" | jq -r "[.[] | {name,certId: .config.ingress?.main.tls[]?.scaleCert} | select(.certId==$_active_cert_id) | .name ] | unique") + _release_length=$(printf "%s" "$_related_name_list" | jq -r "length") + _info "Found $_release_length related chart release in list: $_related_name_list" + for i in $(seq 0 $((_release_length - 1))); do + _release_name=$(echo "$_related_name_list" | jq -r ".[$i]") + _info "Updating certificate from $_active_cert_id to $_cert_id for chart release: $_release_name" + #Read the chart release configuration + _chart_config=$(printf "%s" "$_release_list" | jq -r ".[] | select(.name==\"$_release_name\")") + #Replace the old certificate id with the new one in path .config.ingress.main.tls[].scaleCert. Then update .config.ingress + _updated_chart_config=$(printf "%s" "$_chart_config" | jq "(.config.ingress?.main.tls[]? | select(.scaleCert==$_active_cert_id) | .scaleCert ) |= $_cert_id | .config.ingress ") + _update_chart_result="$(_post "{\"values\" : { \"ingress\" : $_updated_chart_config } }" "$_api_url/chart/release/id/$_release_name" "" "PUT" "application/json")" + _debug3 _update_chart_result "$_update_chart_result" + done + else + _info "Tool 'jq' does not exists, skip chart release checking" + fi + + _info "Deleting old certificate" + _delete_result="$(_post "" "$_api_url/certificate/id/$_active_cert_id" "" "DELETE" "application/json")" + + _debug3 _delete_result "$_delete_result" + + _info "Reloading TrueNAS web UI" + _restart_UI=$(_get "$_api_url/system/general/ui_restart") + _debug2 _restart_UI "$_restart_UI" + + if [ -n "$_add_cert_result" ] && [ -n "$_activate_result" ]; then + return 0 + else + _err "Certificate update was not succesful, please try again with --debug" + return 1 + fi +} 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.sh b/deploy/vault.sh new file mode 100644 index 00000000..569faba2 --- /dev/null +++ b/deploy/vault.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env sh + +# Here is a script to deploy cert to hashicorp vault using curl +# (https://www.vaultproject.io/) +# +# it requires following environment variables: +# +# VAULT_PREFIX - this contains the prefix path in vault +# VAULT_ADDR - vault requires this to find your vault server +# VAULT_SAVE_TOKEN - set to anything if you want to save the token +# VAULT_RENEW_TOKEN - set to anything if you want to renew the token to default TTL before deploying +# VAULT_KV_V2 - set to anything if you are using v2 of the kv engine +# +# additionally, you need to ensure that VAULT_TOKEN is avialable +# to access the vault server + +#returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +vault_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 VAULT_PREFIX + if [ -z "$VAULT_PREFIX" ]; then + _err "VAULT_PREFIX needs to be defined (contains prefix path in vault)" + return 1 + fi + _savedeployconf VAULT_PREFIX "$VAULT_PREFIX" + + _getdeployconf VAULT_ADDR + if [ -z "$VAULT_ADDR" ]; then + _err "VAULT_ADDR needs to be defined (contains vault connection address)" + return 1 + fi + _savedeployconf VAULT_ADDR "$VAULT_ADDR" + + _getdeployconf VAULT_SAVE_TOKEN + _savedeployconf VAULT_SAVE_TOKEN "$VAULT_SAVE_TOKEN" + + _getdeployconf VAULT_RENEW_TOKEN + _savedeployconf VAULT_RENEW_TOKEN "$VAULT_RENEW_TOKEN" + + _getdeployconf VAULT_KV_V2 + _savedeployconf VAULT_KV_V2 "$VAULT_KV_V2" + + _getdeployconf VAULT_TOKEN + if [ -z "$VAULT_TOKEN" ]; then + _err "VAULT_TOKEN needs to be defined" + return 1 + fi + if [ -n "$VAULT_SAVE_TOKEN" ]; then + _savedeployconf VAULT_TOKEN "$VAULT_TOKEN" + fi + + _migratedeployconf FABIO VAULT_FABIO_MODE + + # JSON does not allow multiline strings. + # So replacing new-lines with "\n" here + _ckey=$(sed -z 's/\n/\\n/g' <"$2") + _ccert=$(sed -z 's/\n/\\n/g' <"$3") + _cca=$(sed -z 's/\n/\\n/g' <"$4") + _cfullchain=$(sed -z 's/\n/\\n/g' <"$5") + + export _H1="X-Vault-Token: $VAULT_TOKEN" + + if [ -n "$VAULT_RENEW_TOKEN" ]; then + URL="$VAULT_ADDR/v1/auth/token/renew-self" + _info "Renew the Vault token to default TTL" + if ! _post "" "$URL" >/dev/null; then + _err "Failed to renew the Vault token" + return 1 + fi + fi + + URL="$VAULT_ADDR/v1/$VAULT_PREFIX/$_cdomain" + + if [ -n "$VAULT_FABIO_MODE" ]; then + _info "Writing certificate and key to $URL in Fabio mode" + if [ -n "$VAULT_KV_V2" ]; then + _post "{ \"data\": {\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"} }" "$URL" >/dev/null || return 1 + else + _post "{\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"}" "$URL" >/dev/null || return 1 + fi + else + if [ -n "$VAULT_KV_V2" ]; then + _info "Writing certificate to $URL/cert.pem" + _post "{\"data\": {\"value\": \"$_ccert\"}}" "$URL/cert.pem" >/dev/null || return 1 + _info "Writing key to $URL/cert.key" + _post "{\"data\": {\"value\": \"$_ckey\"}}" "$URL/cert.key" >/dev/null || return 1 + _info "Writing CA certificate to $URL/ca.pem" + _post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/ca.pem" >/dev/null || return 1 + _info "Writing full-chain certificate to $URL/fullchain.pem" + _post "{\"data\": {\"value\": \"$_cfullchain\"}}" "$URL/fullchain.pem" >/dev/null || return 1 + else + _info "Writing certificate to $URL/cert.pem" + _post "{\"value\": \"$_ccert\"}" "$URL/cert.pem" >/dev/null || return 1 + _info "Writing key to $URL/cert.key" + _post "{\"value\": \"$_ckey\"}" "$URL/cert.key" >/dev/null || return 1 + _info "Writing CA certificate to $URL/ca.pem" + _post "{\"value\": \"$_cca\"}" "$URL/ca.pem" >/dev/null || return 1 + _info "Writing full-chain certificate to $URL/fullchain.pem" + _post "{\"value\": \"$_cfullchain\"}" "$URL/fullchain.pem" >/dev/null || return 1 + fi + + # To make it compatible with the wrong ca path `chain.pem` which was used in former versions + if _contains "$(_get "$URL/chain.pem")" "-----BEGIN CERTIFICATE-----"; then + _err "The CA certificate has moved from chain.pem to ca.pem, if you don't depend on chain.pem anymore, you can delete it to avoid this warning" + _info "Updating CA certificate to $URL/chain.pem for backward compatibility" + if [ -n "$VAULT_KV_V2" ]; then + _post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/chain.pem" >/dev/null || return 1 + else + _post "{\"value\": \"$_cca\"}" "$URL/chain.pem" >/dev/null || return 1 + fi + fi + fi + +} diff --git a/deploy/vault_cli.sh b/deploy/vault_cli.sh index 79c25aa2..3ebb8074 100644 --- a/deploy/vault_cli.sh +++ b/deploy/vault_cli.sh @@ -2,12 +2,14 @@ # Here is a script to deploy cert to hashicorp vault # (https://www.vaultproject.io/) -# +# # it requires the vault binary to be available in PATH, and the following # environment variables: -# +# # VAULT_PREFIX - this contains the prefix path in vault # VAULT_ADDR - vault requires this to find your vault server +# VAULT_SAVE_TOKEN - set to anything if you want to save the token +# VAULT_RENEW_TOKEN - set to anything if you want to renew the token to default TTL before deploying # # additionally, you need to ensure that VAULT_TOKEN is avialable or # `vault auth` has applied the appropriate authorization for the vault binary @@ -33,25 +35,70 @@ vault_cli_deploy() { _debug _cfullchain "$_cfullchain" # validate required env vars + _getdeployconf VAULT_PREFIX if [ -z "$VAULT_PREFIX" ]; then _err "VAULT_PREFIX needs to be defined (contains prefix path in vault)" return 1 fi + _savedeployconf VAULT_PREFIX "$VAULT_PREFIX" + _getdeployconf VAULT_ADDR if [ -z "$VAULT_ADDR" ]; then _err "VAULT_ADDR needs to be defined (contains vault connection address)" return 1 fi + _savedeployconf VAULT_ADDR "$VAULT_ADDR" - VAULT_CMD=$(which vault) + _getdeployconf VAULT_SAVE_TOKEN + _savedeployconf VAULT_SAVE_TOKEN "$VAULT_SAVE_TOKEN" + + _getdeployconf VAULT_RENEW_TOKEN + _savedeployconf VAULT_RENEW_TOKEN "$VAULT_RENEW_TOKEN" + + _getdeployconf VAULT_TOKEN + if [ -z "$VAULT_TOKEN" ]; then + _err "VAULT_TOKEN needs to be defined" + return 1 + fi + if [ -n "$VAULT_SAVE_TOKEN" ]; then + _savedeployconf VAULT_TOKEN "$VAULT_TOKEN" + fi + + _migratedeployconf FABIO VAULT_FABIO_MODE + + VAULT_CMD=$(command -v vault) if [ ! $? ]; then _err "cannot find vault binary!" return 1 fi - $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 + if [ -n "$VAULT_RENEW_TOKEN" ]; then + _info "Renew the Vault token to default TTL" + if ! $VAULT_CMD token renew; then + _err "Failed to renew the Vault token" + return 1 + fi + fi + + if [ -n "$VAULT_FABIO_MODE" ]; then + _info "Writing certificate and key to ${VAULT_PREFIX}/${_cdomain} in Fabio mode" + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}" cert=@"$_cfullchain" key=@"$_ckey" || return 1 + else + _info "Writing certificate to ${VAULT_PREFIX}/${_cdomain}/cert.pem" + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/cert.pem" value=@"$_ccert" || return 1 + _info "Writing key to ${VAULT_PREFIX}/${_cdomain}/cert.key" + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/cert.key" value=@"$_ckey" || return 1 + _info "Writing CA certificate to ${VAULT_PREFIX}/${_cdomain}/ca.pem" + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/ca.pem" value=@"$_cca" || return 1 + _info "Writing full-chain certificate to ${VAULT_PREFIX}/${_cdomain}/fullchain.pem" + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/fullchain.pem" value=@"$_cfullchain" || return 1 + + # To make it compatible with the wrong ca path `chain.pem` which was used in former versions + if $VAULT_CMD kv get "${VAULT_PREFIX}/${_cdomain}/chain.pem" >/dev/null; then + _err "The CA certificate has moved from chain.pem to ca.pem, if you don't depend on chain.pem anymore, you can delete it to avoid this warning" + _info "Updating CA certificate to ${VAULT_PREFIX}/${_cdomain}/chain.pem for backward compatibility" + $VAULT_CMD kv put "${VAULT_PREFIX}/${_cdomain}/chain.pem" value=@"$_cca" || return 1 + fi + fi } diff --git a/deploy/vsftpd.sh b/deploy/vsftpd.sh index ed44e709..8cf24e4f 100644 --- a/deploy/vsftpd.sh +++ b/deploy/vsftpd.sh @@ -65,9 +65,9 @@ vsftpd_deploy() { cp "$_vsftpd_conf" "$_backup_conf" _info "Modify vsftpd conf: $_vsftpd_conf" - if _setopt "$_vsftpd_conf" "rsa_cert_file" "=" "$_real_fullchain" \ - && _setopt "$_vsftpd_conf" "rsa_private_key_file" "=" "$_real_key" \ - && _setopt "$_vsftpd_conf" "ssl_enable" "=" "YES"; then + if _setopt "$_vsftpd_conf" "rsa_cert_file" "=" "$_real_fullchain" && + _setopt "$_vsftpd_conf" "rsa_private_key_file" "=" "$_real_key" && + _setopt "$_vsftpd_conf" "ssl_enable" "=" "YES"; then _info "Set config success!" else _err "Config vsftpd server error, please report bug to us." diff --git a/dnsapi/README.md b/dnsapi/README.md index 2cecfa5a..e81f7916 100644 --- a/dnsapi/README.md +++ b/dnsapi/README.md @@ -1,1037 +1,6 @@ # How to use DNS API +DNS api usage: -If your dns provider doesn't provide api access, you can use our dns alias mode: -https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode +https://github.com/acmesh-official/acme.sh/wiki/dnsapi -## 1. Use CloudFlare domain API to automatically issue cert - -First you need to login to your CloudFlare account to get your API key. - -``` -export CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" -export CF_Email="xxxx@sss.com" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_cf -d example.com -d www.example.com -``` - -The `CF_Key` and `CF_Email` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - - -## 2. Use DNSPod.cn domain API to automatically issue cert - -First you need to login to your DNSPod account to get your API Key and ID. - -``` -export DP_Id="1234" -export DP_Key="sADDsdasdgdsf" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_dp -d example.com -d www.example.com -``` - -The `DP_Id` and `DP_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - - -## 3. Use CloudXNS.com domain API to automatically issue cert - -First you need to login to your CloudXNS account to get your API Key and Secret. - -``` -export CX_Key="1234" -export CX_Secret="sADDsdasdgdsf" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_cx -d example.com -d www.example.com -``` - -The `CX_Key` and `CX_Secret` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - - -## 4. Use GoDaddy.com domain API to automatically issue cert - -First you need to login to your GoDaddy account to get your API Key and Secret. - -https://developer.godaddy.com/keys/ - -Please create a Production key, instead of a Test key. - -``` -export GD_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" -export GD_Secret="asdfsdafdsfdsfdsfdsfdsafd" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_gd -d example.com -d www.example.com -``` - -The `GD_Key` and `GD_Secret` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - - -## 5. Use PowerDNS embedded API to automatically issue cert - -First you need to login to your PowerDNS account to enable the API and set your API-Token in the configuration. - -https://doc.powerdns.com/md/httpapi/README/ - -``` -export PDNS_Url="http://ns.example.com:8081" -export PDNS_ServerId="localhost" -export PDNS_Token="0123456789ABCDEF" -export PDNS_Ttl=60 -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_pdns -d example.com -d www.example.com -``` - -The `PDNS_Url`, `PDNS_ServerId`, `PDNS_Token` and `PDNS_Ttl` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - - -## 6. Use OVH/kimsufi/soyoustart/runabove API to automatically issue cert - -https://github.com/Neilpang/acme.sh/wiki/How-to-use-OVH-domain-api - - -## 7. Use nsupdate to automatically issue cert - -First, generate a key for updating the zone -``` -b=$(dnssec-keygen -a hmac-sha512 -b 512 -n USER -K /tmp foo) -cat > /etc/named/keys/update.key < /etc/knot/acme.key -``` - -Include this key in your knot configuration file. - -``` -include: /etc/knot/acme.key -``` - -Next, configure your zone to allow dynamic updates. - -Dynamic updates for the zone are allowed via proper ACL rule with the `update` action. For in-depth instructions, please see [Knot DNS's documentation](https://www.knot-dns.cz/documentation/). - -``` -acl: - - id: acme_acl - address: 192.168.1.0/24 - key: acme_key - action: update - -zone: - - domain: example.com - file: example.com.zone - acl: acme_acl -``` - -Finally, make the DNS server and TSIG Key available to `acme.sh` - -``` -export KNOT_SERVER="dns.example.com" -export KNOT_KEY=`grep \# /etc/knot/acme.key | cut -d' ' -f2` -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_knot -d example.com -d www.example.com -``` - -The `KNOT_SERVER` and `KNOT_KEY` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 20. Use DigitalOcean API (native) - -You need to obtain a read and write capable API key from your DigitalOcean account. See: https://www.digitalocean.com/help/api/ - -``` -export DO_API_KEY="75310dc4ca779ac39a19f6355db573b49ce92ae126553ebd61ac3a3ae34834cc" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_dgon -d example.com -d www.example.com -``` - -## 21. Use ClouDNS.net API - -You need to set the HTTP API user ID and password credentials. See: https://www.cloudns.net/wiki/article/42/. For security reasons, it's recommended to use a sub user ID that only has access to the necessary zones, as a regular API user has access to your entire account. - -``` -# Use this for a sub auth ID -export CLOUDNS_SUB_AUTH_ID=XXXXX -# Use this for a regular auth ID -#export CLOUDNS_AUTH_ID=XXXXX -export CLOUDNS_AUTH_PASSWORD="YYYYYYYYY" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_cloudns -d example.com -d www.example.com -``` -The `CLOUDNS_AUTH_ID` and `CLOUDNS_AUTH_PASSWORD` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 22. Use Infoblox API - -First you need to create/obtain API credentials on your Infoblox appliance. - -``` -export Infoblox_Creds="username:password" -export Infoblox_Server="ip or fqdn of infoblox appliance" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_infoblox -d example.com -d www.example.com -``` - -Note: This script will automatically create and delete the ephemeral txt record. -The `Infoblox_Creds` and `Infoblox_Server` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - - -## 23. Use VSCALE API - -First you need to create/obtain API tokens on your [settings panel](https://vscale.io/panel/settings/tokens/). - -``` -VSCALE_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_vscale -d example.com -d www.example.com -``` - -## 24. Use Dynu API - -First you need to create/obtain API credentials from your Dynu account. See: https://www.dynu.com/resources/api/documentation - -``` -export Dynu_ClientId="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -export Dynu_Secret="yyyyyyyyyyyyyyyyyyyyyyyyy" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_dynu -d example.com -d www.example.com -``` - -The `Dynu_ClientId` and `Dynu_Secret` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 25. Use DNSimple API - -First you need to login to your DNSimple account and generate a new oauth token. - -https://dnsimple.com/a/{your account id}/account/access_tokens - -Note that this is an _account_ token and not a user token. The account token is -needed to infer the `account_id` used in requests. A user token will not be able -to determine the correct account to use. - -``` -export DNSimple_OAUTH_TOKEN="sdfsdfsdfljlbjkljlkjsdfoiwje" -``` - -To issue the cert just specify the `dns_dnsimple` API. - -``` -acme.sh --issue --dns dns_dnsimple -d example.com -``` - -The `DNSimple_OAUTH_TOKEN` will be saved in `~/.acme.sh/account.conf` and will -be reused when needed. - -If you have any issues with this integration please report them to -https://github.com/pho3nixf1re/acme.sh/issues. - -## 26. Use NS1.com API - -``` -export NS1_Key="fdmlfsdklmfdkmqsdfk" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_nsone -d example.com -d www.example.com -``` - -## 27. Use DuckDNS.org API - -``` -export DuckDNS_Token="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" -``` - -Please note that since DuckDNS uses StartSSL as their cert provider, thus ---insecure may need to be used when issuing certs: -``` -acme.sh --insecure --issue --dns dns_duckdns -d mydomain.duckdns.org -``` - -For issues, please report to https://github.com/raidenii/acme.sh/issues. - -## 28. Use Name.com API - -Create your API token here: https://www.name.com/account/settings/api - -Note: `Namecom_Username` should be your Name.com username and not the token name. If you accidentally run the script with the token name as the username see `~/.acme.sh/account.conf` to fix the issue - -``` -export Namecom_Username="testuser" -export Namecom_Token="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -``` - -And now you can issue certs with: - -``` -acme.sh --issue --dns dns_namecom -d example.com -d www.example.com -``` - -For issues, please report to https://github.com/raidenii/acme.sh/issues. - -## 29. Use Dyn Managed DNS API to automatically issue cert - -First, login to your Dyn Managed DNS account: https://portal.dynect.net/login/ - -It is recommended to add a new user specific for API access. - -The minimum "Zones & Records Permissions" required are: -``` -RecordAdd -RecordUpdate -RecordDelete -RecordGet -ZoneGet -ZoneAddNode -ZoneRemoveNode -ZonePublish -``` - -Pass the API user credentials to the environment: -``` -export DYN_Customer="customer" -export DYN_Username="apiuser" -export DYN_Password="secret" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_dyn -d example.com -d www.example.com -``` - -The `DYN_Customer`, `DYN_Username` and `DYN_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 30. Use pdd.yandex.ru API - -``` -export PDD_Token="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -``` - -Follow these instructions to get the token for your domain https://tech.yandex.com/domain/doc/concepts/access-docpage/ -``` -acme.sh --issue --dns dns_yandex -d mydomain.example.org -``` - -For issues, please report to https://github.com/non7top/acme.sh/issues. - -## 31. Use Hurricane Electric - -Hurricane Electric (https://dns.he.net/) doesn't have an API so just set your login credentials like so: - -``` -export HE_Username="yourusername" -export HE_Password="password" -``` - -Then you can issue your certificate: - -``` -acme.sh --issue --dns dns_he -d example.com -d www.example.com -``` - -The `HE_Username` and `HE_Password` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -Please report any issues to https://github.com/angel333/acme.sh or to . - -## 32. Use UnoEuro API to automatically issue cert - -First you need to login to your UnoEuro account to get your API key. - -``` -export UNO_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" -export UNO_User="UExxxxxx" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_unoeuro -d example.com -d www.example.com -``` - -The `UNO_Key` and `UNO_User` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 33. Use INWX - -[INWX](https://www.inwx.de/) offers an [xmlrpc api](https://www.inwx.de/de/help/apidoc) with your standard login credentials, set them like so: - -``` -export INWX_User="yourusername" -export INWX_Password="password" -``` - -Then you can issue your certificates with: - -``` -acme.sh --issue --dns dns_inwx -d example.com -d www.example.com -``` - -The `INWX_User` and `INWX_Password` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -If your account is secured by mobile tan you have also defined the shared secret. - -``` -export INWX_Shared_Secret="shared secret" -``` - -You may need to re-enable the mobile tan to gain the shared secret. - -## 34. User Servercow API v1 - -Create a new user from the servercow control center. Don't forget to activate **DNS API** for this user. - -``` -export SERVERCOW_API_Username=username -export SERVERCOW_API_Password=password -``` - -Now you cann issue a cert: - -``` -acme.sh --issue --dns dns_servercow -d example.com -d www.example.com -``` -Both, `SERVERCOW_API_Username` and `SERVERCOW_API_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 35. Use Namesilo.com API - -You'll need to generate an API key at https://www.namesilo.com/account_api.php -Optionally you may restrict the access to an IP range there. - -``` -export Namesilo_Key="xxxxxxxxxxxxxxxxxxxxxxxx" -``` - -And now you can issue certs with: - -``` -acme.sh --issue --dns dns_namesilo --dnssleep 900 -d example.com -d www.example.com -``` - -## 36. Use autoDNS (InternetX) - -[InternetX](https://www.internetx.com/) offers an [xml api](https://help.internetx.com/display/API/AutoDNS+XML-API) with your standard login credentials, set them like so: - -``` -export AUTODNS_USER="yourusername" -export AUTODNS_PASSWORD="password" -export AUTODNS_CONTEXT="context" -``` - -Then you can issue your certificates with: - -``` -acme.sh --issue --dns dns_autodns -d example.com -d www.example.com -``` - -The `AUTODNS_USER`, `AUTODNS_PASSWORD` and `AUTODNS_CONTEXT` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 37. Use Azure DNS - -You have to create a service principal first. See:[How to use Azure DNS](../../../wiki/How-to-use-Azure-DNS) - -``` -export AZUREDNS_SUBSCRIPTIONID="12345678-9abc-def0-1234-567890abcdef" -export AZUREDNS_TENANTID="11111111-2222-3333-4444-555555555555" -export AZUREDNS_APPID="3b5033b5-7a66-43a5-b3b9-a36b9e7c25ed" -export AZUREDNS_CLIENTSECRET="1b0224ef-34d4-5af9-110f-77f527d561bd" -``` - -Then you can issue your certificates with: - -``` -acme.sh --issue --dns dns_azure -d example.com -d www.example.com -``` - -`AZUREDNS_SUBSCRIPTIONID`, `AZUREDNS_TENANTID`,`AZUREDNS_APPID` and `AZUREDNS_CLIENTSECRET` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 38. Use selectel.com(selectel.ru) domain API to automatically issue cert - -First you need to login to your account to get your API key from: https://my.selectel.ru/profile/apikeys. - -```sh -export SL_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" - -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_selectel -d example.com -d www.example.com -``` - -The `SL_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 39. Use zonomi.com domain API to automatically issue cert - -First you need to login to your account to find your API key from: http://zonomi.com/app/dns/dyndns.jsp - -Your will find your api key in the example urls: - -```sh -https://zonomi.com/app/dns/dyndns.jsp?host=example.com&api_key=1063364558943540954358668888888888 -``` - -```sh -export ZM_Key="1063364558943540954358668888888888" - -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_zonomi -d example.com -d www.example.com -``` - -The `ZM_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 40. Use DreamHost DNS API - -DNS API keys may be created at https://panel.dreamhost.com/?tree=home.api. -Ensure the created key has add and remove privelages. - -``` -export DH_API_KEY="" -acme.sh --issue --dns dns_dreamhost -d example.com -d www.example.com -``` - -The 'DH_API_KEY' will be saved in `~/.acme.sh/account.conf` and will -be reused when needed. - -## 41. Use DirectAdmin API -The DirectAdmin interface has it's own Let's encrypt functionality, but this -script can be used to generate certificates for names which are not hosted on -DirectAdmin - -User must provide login data and URL to the DirectAdmin incl. port. -You can create an user which only has access to - -- CMD_API_DNS_CONTROL -- CMD_API_SHOW_DOMAINS - -By using the Login Keys function. -See also https://www.directadmin.com/api.php and https://www.directadmin.com/features.php?id=1298 - -``` -export DA_Api="https://remoteUser:remotePassword@da.domain.tld:8443" -export DA_Api_Insecure=1 -``` -Set `DA_Api_Insecure` to 1 for insecure and 0 for secure -> difference is whether ssl cert is checked for validity (0) or whether it is just accepted (1) - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_da -d example.com -d www.example.com -``` - -The `DA_Api` and `DA_Api_Insecure` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 42. Use KingHost DNS API - -API access must be enabled at https://painel.kinghost.com.br/painel.api.php - -``` -export KINGHOST_Username="yourusername" -export KINGHOST_Password="yourpassword" -acme.sh --issue --dns dns_kinghost -d example.com -d *.example.com -``` - -The `KINGHOST_username` and `KINGHOST_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 43. Use Zilore DNS API - -First, get your API key at https://my.zilore.com/account/api - -``` -export Zilore_Key="5dcad3a2-36cb-50e8-cb92-000002f9" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_zilore -d example.com -d *.example.com -``` - -The `Zilore_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 44. Use Loopia.se API -User must provide login credentials to the Loopia API. -The user needs the following permissions: - -- addSubdomain -- updateZoneRecord -- getDomains -- removeSubdomain - -Set the login credentials: -``` -export LOOPIA_User="user@loopiaapi" -export LOOPIA_Password="password" -``` - -And to issue a cert: -``` -acme.sh --issue --dns dns_loopia -d example.com -d *.example.com -``` - -The username and password will be saved in `~/.acme.sh/account.conf` and will be reused when needed. -## 45. Use ACME DNS API - -ACME DNS is a limited DNS server with RESTful HTTP API to handle ACME DNS challenges easily and securely. -https://github.com/joohoi/acme-dns - -``` -export ACMEDNS_UPDATE_URL="https://auth.acme-dns.io/update" -export ACMEDNS_USERNAME="" -export ACMEDNS_PASSWORD="" -export ACMEDNS_SUBDOMAIN="" - -acme.sh --issue --dns dns_acmedns -d example.com -d www.example.com -``` - -The credentials will be saved in `~/.acme.sh/account.conf` and will -be reused when needed. -## 46. Use TELE3 API - -First you need to login to your TELE3 account to set your API-KEY. -https://www.tele3.cz/system-acme-api.html - -``` -export TELE3_Key="MS2I4uPPaI..." -export TELE3_Secret="kjhOIHGJKHg" - -acme.sh --issue --dns dns_tele3 -d example.com -d *.example.com -``` - -The TELE3_Key and TELE3_Secret will be saved in ~/.acme.sh/account.conf and will be reused when needed. - -## 47. Use Euserv.eu API - -First you need to login to your euserv.eu account and activate your API Administration (API Verwaltung). -[https://support.euserv.com](https://support.euserv.com) - -Once you've activate, login to your API Admin Interface and create an API account. -Please specify the scope (active groups: domain) and assign the allowed IPs. - -``` -export EUSERV_Username="99999.user123" -export EUSERV_Password="Asbe54gHde" -``` - -Ok, let's issue a cert now: (Be aware to use the `--insecure` flag, cause euserv.eu is still using self-signed certificates!) -``` -acme.sh --issue --dns dns_euserv -d example.com -d *.example.com --insecure -``` - -The `EUSERV_Username` and `EUSERV_Password` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -Please report any issues to https://github.com/initit/acme.sh or to - -## 48. Use DNSPod.com domain API to automatically issue cert - -First you need to get your API Key and ID by this [get-the-user-token](https://www.dnspod.com/docs/info.html#get-the-user-token). - -``` -export DPI_Id="1234" -export DPI_Key="sADDsdasdgdsf" -``` - -Ok, let's issue a cert now: -``` -acme.sh --issue --dns dns_dpi -d example.com -d www.example.com -``` - -The `DPI_Id` and `DPI_Key` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 49. Use Google Cloud DNS API to automatically issue cert - -First you need to authenticate to gcloud. - -``` -gcloud init -``` - -**The `dns_gcloud` script uses the active gcloud configuration and credentials.** -There is no logic inside `dns_gcloud` to override the project and other settings. -If needed, create additional [gcloud configurations](https://cloud.google.com/sdk/gcloud/reference/topic/configurations). -You can change the configuration being used without *activating* it; simply set the `CLOUDSDK_ACTIVE_CONFIG_NAME` environment variable. - -To issue a certificate you can: -``` -export CLOUDSDK_ACTIVE_CONFIG_NAME=default # see the note above -acme.sh --issue --dns dns_gcloud -d example.com -d '*.example.com' -``` - -`dns_gcloud` also supports [DNS alias mode](https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode). - -## 50. Use ConoHa API - -First you need to login to your ConoHa account to get your API credentials. - -``` -export CONOHA_Username="xxxxxx" -export CONOHA_Password="xxxxxx" -export CONOHA_TenantId="xxxxxx" -export CONOHA_IdentityServiceApi="https://identity.xxxx.conoha.io/v2.0" -``` - -To issue a cert: -``` -acme.sh --issue --dns dns_conoha -d example.com -d www.example.com -``` - -The `CONOHA_Username`, `CONOHA_Password`, `CONOHA_TenantId` and `CONOHA_IdentityServiceApi` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 51. Use netcup DNS API to automatically issue cert - -First you need to login in your CCP account to get your API Key and API Password. -``` -export NC_Apikey="" -export NC_Apipw="" -export NC_CID="" -``` - -Now, let's issue a cert: -``` -acme.sh --issue --dns dns_netcup -d example.com -d www.example.com -``` - -The `NC_Apikey`,`NC_Apipw` and `NC_CID` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -## 52. Use GratisDNS.dk - -GratisDNS.dk (https://gratisdns.dk/) does not provide an API to update DNS records (other than IPv4 and IPv6 -dynamic DNS addresses). The acme.sh plugin therefore retrieves and updates domain TXT records by logging -into the GratisDNS website to read the HTML and posting updates as HTTP. The plugin needs to know your -userid and password for the GratisDNS website. - -```sh -export GDNSDK_Username="..." -export GDNSDK_Password="..." -``` -The username and password will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - - -Now you can issue a certificate. - -Note: It usually takes a few minutes (usually 3-4 minutes) before the changes propagates to gratisdns.dk nameservers (ns3.gratisdns.dk often are slow), -and in rare cases I have seen over 5 minutes before google DNS catches it. Therefor a DNS sleep of at least 300 seconds are recommended- - -```sh -acme.sh --issue --dns dns_gdnsdk --dnssleep 300 -d example.com -d *.example.com -``` - -## 53. Use Namecheap - -You will need your namecheap username, API KEY (https://www.namecheap.com/support/api/intro.aspx) and your external IP address (or an URL to get it), this IP will need to be whitelisted at Namecheap. -Due to Namecheap's API limitation all the records of your domain will be read and re applied, make sure to have a backup of your records you could apply if any issue would arise. - -```sh -export NAMECHEAP_USERNAME="..." -export NAMECHEAP_API_KEY="..." -export NAMECHEAP_SOURCEIP="..." -``` - -NAMECHEAP_SOURCEIP can either be an IP address or an URL to provide it (e.g. https://ifconfig.co/ip). - -The username and password will be saved in `~/.acme.sh/account.conf` and will be reused when needed. - -Now you can issue a certificate. - -```sh -acme.sh --issue --dns dns_namecheap -d example.com -d *.example.com -``` - -# Use custom API - -If your API is not supported yet, you can write your own DNS API. - -Let's assume you want to name it 'myapi': - -1. Create a bash script named `~/.acme.sh/dns_myapi.sh`, -2. In the script you must have a function named `dns_myapi_add()` which will be called by acme.sh to add the DNS records. -3. Then you can use your API to issue cert like this: - -``` -acme.sh --issue --dns dns_myapi -d example.com -d www.example.com -``` - -For more details, please check our sample script: [dns_myapi.sh](dns_myapi.sh) - -See: https://github.com/Neilpang/acme.sh/wiki/DNS-API-Dev-Guide - -# Use lexicon DNS API - -https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api diff --git a/dnsapi/dns_1984hosting.sh b/dnsapi/dns_1984hosting.sh new file mode 100755 index 00000000..e4ef2e4b --- /dev/null +++ b/dnsapi/dns_1984hosting.sh @@ -0,0 +1,273 @@ +#!/usr/bin/env sh +# This file name is "dns_1984hosting.sh" +# So, here must be a method dns_1984hosting_add() +# Which will be called by acme.sh to add the txt record to your api system. +# returns 0 means success, otherwise error. + +# Author: Adrian Fedoreanu +# Report Bugs here: https://github.com/acmesh-official/acme.sh +# or here... https://github.com/acmesh-official/acme.sh/issues/2851 + +######## Public functions ##################### + +# Export 1984HOSTING username and password in following variables +# +# One984HOSTING_Username=username +# One984HOSTING_Password=password +# +# username/password and csrftoken/sessionid cookies are saved in ~/.acme.sh/account.conf + +# Usage: dns_1984hosting_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Add a text record. +dns_1984hosting_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Add TXT record using 1984Hosting." + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _1984hosting_login; then + _err "1984Hosting login failed for user $One984HOSTING_Username. Check $HTTP_HEADER file." + return 1 + fi + + _debug "First detect the root zone." + if ! _get_root "$fulldomain"; then + _err "Invalid domain '$fulldomain'." + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Add TXT record $fulldomain with value '$txtvalue'." + value="$(printf '%s' "$txtvalue" | _url_encode)" + url="https://1984.hosting/domains/entry/" + + postdata="entry=new" + postdata="$postdata&type=TXT" + postdata="$postdata&ttl=900" + postdata="$postdata&zone=$_domain" + postdata="$postdata&host=$_sub_domain" + postdata="$postdata&rdata=%22$value%22" + _debug2 postdata "$postdata" + + _authpost "$postdata" "$url" + 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" "html>"; then + _err "1984Hosting failed to add TXT record for $_sub_domain. Check $HTTP_HEADER file." + return 1 + elif _contains "$_response" '"auth": false'; then + _err "1984Hosting failed to add TXT record for $_sub_domain. Invalid or expired cookie." + return 1 + fi + + _info "Added acme challenge TXT record for $fulldomain at 1984Hosting." + return 0 +} + +# Usage: fulldomain txtvalue +# Remove the txt record after validation. +dns_1984hosting_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Delete TXT record using 1984Hosting." + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _1984hosting_login; then + _err "1984Hosting login failed for user $One984HOSTING_Username. Check $HTTP_HEADER file." + return 1 + fi + + _debug "First detect the root zone." + if ! _get_root "$fulldomain"; then + _err "Invalid domain '$fulldomain'." + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug "Delete $fulldomain TXT record." + + url="https://1984.hosting/domains" + if ! _get_zone_id "$url" "$_domain"; then + _err "Invalid zone '$_domain'." + return 1 + fi + + _htmlget "$url/$_zone_id" "$txtvalue" + entry_id="$(echo "$_response" | _egrep_o 'entry_[0-9]+' | sed 's/entry_//')" + _debug2 entry_id "$entry_id" + if [ -z "$entry_id" ]; then + _err "Error getting TXT entry_id for $1." + return 1 + fi + + _authpost "entry=$entry_id" "$url/delentry/" + if ! _contains "$_response" '"ok": true'; then + _err "1984Hosting failed to delete TXT record for $entry_id bad RC from _post." + return 1 + fi + + _info "Deleted acme challenge TXT record for $fulldomain at 1984Hosting." + return 0 +} + +#################### Private functions below ################################## +_1984hosting_login() { + if ! _check_credentials; then return 1; fi + + if _check_cookies; then + _debug "Already logged in." + return 0 + fi + + _debug "Login to 1984Hosting as user $One984HOSTING_Username." + username=$(printf '%s' "$One984HOSTING_Username" | _url_encode) + password=$(printf '%s' "$One984HOSTING_Password" | _url_encode) + url="https://1984.hosting/api/auth/" + + _get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken" + csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')" + sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')" + + if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then + _err "One or more cookies are empty: '$csrftoken', '$sessionid'." + return 1 + fi + + export _H1="Cookie: $csrftoken; $sessionid" + export _H2="Referer: https://1984.hosting/accounts/login/" + csrf_header=$(echo "$csrftoken" | sed 's/csrftoken=//' | _head_n 1) + export _H3="X-CSRFToken: $csrf_header" + + response="$(_post "username=$username&password=$password&otpkey=" $url)" + response="$(echo "$response" | _normalizeJson)" + _debug2 response "$response" + + if _contains "$response" '"loggedin": true'; then + One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')" + One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')" + export One984HOSTING_SESSIONID_COOKIE + export One984HOSTING_CSRFTOKEN_COOKIE + _saveaccountconf_mutable One984HOSTING_Username "$One984HOSTING_Username" + _saveaccountconf_mutable One984HOSTING_Password "$One984HOSTING_Password" + _saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE" + _saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE" + return 0 + fi + return 1 +} + +_check_credentials() { + One984HOSTING_Username="${One984HOSTING_Username:-$(_readaccountconf_mutable One984HOSTING_Username)}" + One984HOSTING_Password="${One984HOSTING_Password:-$(_readaccountconf_mutable One984HOSTING_Password)}" + if [ -z "$One984HOSTING_Username" ] || [ -z "$One984HOSTING_Password" ]; then + One984HOSTING_Username="" + One984HOSTING_Password="" + _clearaccountconf_mutable One984HOSTING_Username + _clearaccountconf_mutable One984HOSTING_Password + _err "You haven't specified 1984Hosting username or password yet." + _err "Please export as One984HOSTING_Username / One984HOSTING_Password and try again." + return 1 + fi + return 0 +} + +_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://1984.hosting/api/auth/" + if _contains "$_response" '"ok": true'; then + _debug "Cached cookies still valid." + return 0 + fi + + _debug "Cached cookies no longer valid. Clearing cookies." + One984HOSTING_SESSIONID_COOKIE="" + One984HOSTING_CSRFTOKEN_COOKIE="" + _clearaccountconf_mutable One984HOSTING_SESSIONID_COOKIE + _clearaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE + return 1 +} + +# _acme-challenge.www.domain.com +# Returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain="$1" + i=1 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + + # not valid + if [ -z "$h" ]; then + return 1 + fi + + _authget "https://1984.hosting/domains/soacheck/?zone=$h&nameserver=ns0.1984.is." + if _contains "$_response" "serial" && ! _contains "$_response" "null"; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain="$h" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +# Usage: _get_zone_id url domain.com +# Returns zone id for domain.com +_get_zone_id() { + url=$1 + domain=$2 + _htmlget "$url" "$domain" + _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_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_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE" + _response=$(_get "$1" | grep "$2") + if _contains "$_response" "@$2"; then + _response=$(echo "$_response" | grep -v "[@]" | _head_n 1) + fi + _debug2 _response "$_response" +} + +# Add extra headers to request +_authpost() { + url="https://1984.hosting/domains" + _get_zone_id "$url" "$_domain" + csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")" + export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE" + export _H2="Referer: https://1984.hosting/domains/$_zone_id" + export _H3="X-CSRFToken: $csrf_header" + _response="$(_post "$1" "$2" | _normalizeJson)" + _debug2 _response "$_response" +} diff --git a/dnsapi/dns_acmedns.sh b/dnsapi/dns_acmedns.sh old mode 100644 new mode 100755 index 9b3efa48..057f9742 --- a/dnsapi/dns_acmedns.sh +++ b/dnsapi/dns_acmedns.sh @@ -1,31 +1,70 @@ #!/usr/bin/env sh # #Author: Wolfgang Ebner -#Report Bugs here: https://github.com/webner/acme.sh +#Author: Sven Neubuaer +#Report Bugs here: https://github.com/dampfklon/acme.sh +# +# Usage: +# export ACMEDNS_BASE_URL="https://auth.acme-dns.io" +# +# You can optionally define an already existing account: +# +# export ACMEDNS_USERNAME="" +# export ACMEDNS_PASSWORD="" +# export ACMEDNS_SUBDOMAIN="" # ######## Public functions ##################### #Usage: dns_acmedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record dns_acmedns_add() { fulldomain=$1 txtvalue=$2 _info "Using acme-dns" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" + _debug "fulldomain $fulldomain" + _debug "txtvalue $txtvalue" - ACMEDNS_UPDATE_URL="${ACMEDNS_UPDATE_URL:-$(_readaccountconf_mutable ACMEDNS_UPDATE_URL)}" + #for compatiblity from account conf ACMEDNS_USERNAME="${ACMEDNS_USERNAME:-$(_readaccountconf_mutable ACMEDNS_USERNAME)}" + _clearaccountconf_mutable ACMEDNS_USERNAME ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readaccountconf_mutable ACMEDNS_PASSWORD)}" + _clearaccountconf_mutable ACMEDNS_PASSWORD ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readaccountconf_mutable ACMEDNS_SUBDOMAIN)}" + _clearaccountconf_mutable ACMEDNS_SUBDOMAIN - if [ "$ACMEDNS_UPDATE_URL" = "" ]; then - ACMEDNS_UPDATE_URL="https://auth.acme-dns.io/update" + ACMEDNS_BASE_URL="${ACMEDNS_BASE_URL:-$(_readdomainconf ACMEDNS_BASE_URL)}" + ACMEDNS_USERNAME="${ACMEDNS_USERNAME:-$(_readdomainconf ACMEDNS_USERNAME)}" + ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readdomainconf ACMEDNS_PASSWORD)}" + ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readdomainconf ACMEDNS_SUBDOMAIN)}" + + if [ "$ACMEDNS_BASE_URL" = "" ]; then + ACMEDNS_BASE_URL="https://auth.acme-dns.io" fi - _saveaccountconf_mutable ACMEDNS_UPDATE_URL "$ACMEDNS_UPDATE_URL" - _saveaccountconf_mutable ACMEDNS_USERNAME "$ACMEDNS_USERNAME" - _saveaccountconf_mutable ACMEDNS_PASSWORD "$ACMEDNS_PASSWORD" - _saveaccountconf_mutable ACMEDNS_SUBDOMAIN "$ACMEDNS_SUBDOMAIN" + ACMEDNS_UPDATE_URL="$ACMEDNS_BASE_URL/update" + ACMEDNS_REGISTER_URL="$ACMEDNS_BASE_URL/register" + + if [ -z "$ACMEDNS_USERNAME" ] || [ -z "$ACMEDNS_PASSWORD" ]; then + response="$(_post "" "$ACMEDNS_REGISTER_URL" "" "POST")" + _debug response "$response" + ACMEDNS_USERNAME=$(echo "$response" | sed -n 's/^{.*\"username\":[ ]*\"\([^\"]*\)\".*}/\1/p') + _debug "received username: $ACMEDNS_USERNAME" + ACMEDNS_PASSWORD=$(echo "$response" | sed -n 's/^{.*\"password\":[ ]*\"\([^\"]*\)\".*}/\1/p') + _debug "received password: $ACMEDNS_PASSWORD" + ACMEDNS_SUBDOMAIN=$(echo "$response" | sed -n 's/^{.*\"subdomain\":[ ]*\"\([^\"]*\)\".*}/\1/p') + _debug "received subdomain: $ACMEDNS_SUBDOMAIN" + ACMEDNS_FULLDOMAIN=$(echo "$response" | sed -n 's/^{.*\"fulldomain\":[ ]*\"\([^\"]*\)\".*}/\1/p') + _info "##########################################################" + _info "# Create $fulldomain CNAME $ACMEDNS_FULLDOMAIN DNS entry #" + _info "##########################################################" + _info "Press enter to continue... " + read -r _ + fi + + _savedomainconf ACMEDNS_BASE_URL "$ACMEDNS_BASE_URL" + _savedomainconf ACMEDNS_USERNAME "$ACMEDNS_USERNAME" + _savedomainconf ACMEDNS_PASSWORD "$ACMEDNS_PASSWORD" + _savedomainconf ACMEDNS_SUBDOMAIN "$ACMEDNS_SUBDOMAIN" export _H1="X-Api-User: $ACMEDNS_USERNAME" export _H2="X-Api-Key: $ACMEDNS_PASSWORD" @@ -48,8 +87,8 @@ dns_acmedns_rm() { fulldomain=$1 txtvalue=$2 _info "Using acme-dns" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" + _debug "fulldomain $fulldomain" + _debug "txtvalue $txtvalue" } #################### Private functions below ################################## diff --git a/dnsapi/dns_acmeproxy.sh b/dnsapi/dns_acmeproxy.sh new file mode 100644 index 00000000..9d5533f9 --- /dev/null +++ b/dnsapi/dns_acmeproxy.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env sh + +## Acmeproxy DNS provider to be used with acmeproxy (https://github.com/mdbraber/acmeproxy) +## API integration by Maarten den Braber +## +## Report any bugs via https://github.com/mdbraber/acme.sh + +dns_acmeproxy_add() { + fulldomain="${1}" + txtvalue="${2}" + action="present" + + _debug "Calling: _acmeproxy_request() '${fulldomain}' '${txtvalue}' '${action}'" + _acmeproxy_request "$fulldomain" "$txtvalue" "$action" +} + +dns_acmeproxy_rm() { + fulldomain="${1}" + txtvalue="${2}" + action="cleanup" + + _debug "Calling: _acmeproxy_request() '${fulldomain}' '${txtvalue}' '${action}'" + _acmeproxy_request "$fulldomain" "$txtvalue" "$action" +} + +_acmeproxy_request() { + + ## Nothing to see here, just some housekeeping + fulldomain=$1 + txtvalue=$2 + action=$3 + + _info "Using acmeproxy" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + ACMEPROXY_ENDPOINT="${ACMEPROXY_ENDPOINT:-$(_readaccountconf_mutable ACMEPROXY_ENDPOINT)}" + ACMEPROXY_USERNAME="${ACMEPROXY_USERNAME:-$(_readaccountconf_mutable ACMEPROXY_USERNAME)}" + ACMEPROXY_PASSWORD="${ACMEPROXY_PASSWORD:-$(_readaccountconf_mutable ACMEPROXY_PASSWORD)}" + + ## Check for the endpoint + if [ -z "$ACMEPROXY_ENDPOINT" ]; then + ACMEPROXY_ENDPOINT="" + _err "You didn't specify the endpoint" + _err "Please set them via 'export ACMEPROXY_ENDPOINT=https://ip:port' and try again." + return 1 + fi + + ## Save the credentials to the account file + _saveaccountconf_mutable ACMEPROXY_ENDPOINT "$ACMEPROXY_ENDPOINT" + _saveaccountconf_mutable ACMEPROXY_USERNAME "$ACMEPROXY_USERNAME" + _saveaccountconf_mutable ACMEPROXY_PASSWORD "$ACMEPROXY_PASSWORD" + + if [ -z "$ACMEPROXY_USERNAME" ] || [ -z "$ACMEPROXY_PASSWORD" ]; then + _info "ACMEPROXY_USERNAME and/or ACMEPROXY_PASSWORD not set - using without client authentication! Make sure you're using server authentication (e.g. IP-based)" + export _H1="Accept: application/json" + export _H2="Content-Type: application/json" + else + ## Base64 encode the credentials + credentials=$(printf "%b" "$ACMEPROXY_USERNAME:$ACMEPROXY_PASSWORD" | _base64) + + ## Construct the HTTP Authorization header + export _H1="Authorization: Basic $credentials" + export _H2="Accept: application/json" + export _H3="Content-Type: application/json" + fi + + ## Add the challenge record to the acmeproxy grid member + response="$(_post "{\"fqdn\": \"$fulldomain.\", \"value\": \"$txtvalue\"}" "$ACMEPROXY_ENDPOINT/$action" "" "POST")" + + ## Let's see if we get something intelligible back from the unit + if echo "$response" | grep "\"$txtvalue\"" >/dev/null; then + _info "Successfully updated the txt record" + return 0 + else + _err "Error encountered during record addition" + _err "$response" + return 1 + fi + +} + +#################### Private functions below ################################## diff --git a/dnsapi/dns_active24.sh b/dnsapi/dns_active24.sh new file mode 100755 index 00000000..862f734f --- /dev/null +++ b/dnsapi/dns_active24.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env sh + +#ACTIVE24_Token="sdfsdfsdfljlbjkljlkjsdfoiwje" + +ACTIVE24_Api="https://api.active24.com" + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +dns_active24_add() { + fulldomain=$1 + txtvalue=$2 + + _active24_init + + _info "Adding txt record" + if _active24_rest POST "dns/$_domain/txt/v1" "{\"name\":\"$_sub_domain\",\"text\":\"$txtvalue\",\"ttl\":0}"; then + if _contains "$response" "errors"; then + _err "Add txt record error." + return 1 + else + _info "Added, OK" + return 0 + fi + fi + _err "Add txt record error." + return 1 +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +dns_active24_rm() { + fulldomain=$1 + txtvalue=$2 + + _active24_init + + _debug "Getting txt records" + _active24_rest GET "dns/$_domain/records/v1" + + if _contains "$response" "errors"; then + _err "Error" + return 1 + fi + + hash_ids=$(echo "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o "hashId\":\"[^\"]+" | cut -c10-) + + for hash_id in $hash_ids; do + _debug "Removing hash_id" "$hash_id" + if _active24_rest DELETE "dns/$_domain/$hash_id/v1" ""; then + if _contains "$response" "errors"; then + _err "Unable to remove txt record." + return 1 + else + _info "Removed txt record." + return 0 + fi + fi + done + + _err "No txt records found." + return 1 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + + if ! _active24_rest GET "dns/domains/v1"; then + return 1 + fi + + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug "h" "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if _contains "$response" "\"$h\"" >/dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_active24_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Authorization: Bearer $ACTIVE24_Token" + + if [ "$m" != "GET" ]; then + _debug "data" "$data" + response="$(_post "$data" "$ACTIVE24_Api/$ep" "" "$m" "application/json")" + else + response="$(_get "$ACTIVE24_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} + +_active24_init() { + ACTIVE24_Token="${ACTIVE24_Token:-$(_readaccountconf_mutable ACTIVE24_Token)}" + if [ -z "$ACTIVE24_Token" ]; then + ACTIVE24_Token="" + _err "You didn't specify a Active24 api token yet." + _err "Please create the token and try again." + return 1 + fi + + _saveaccountconf_mutable ACTIVE24_Token "$ACTIVE24_Token" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" +} diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh index 543a0a54..c69839dc 100755 --- a/dnsapi/dns_ali.sh +++ b/dnsapi/dns_ali.sh @@ -117,7 +117,7 @@ _ali_urlencode() { _ali_nonce() { #_head_n 1 /dev/null +} + +#################### Private functions below ################################## + +_is_uuid() { + pattern='^\{?[A-Z0-9a-z]{8}-[A-Z0-9a-z]{4}-[A-Z0-9a-z]{4}-[A-Z0-9a-z]{4}-[A-Z0-9a-z]{12}\}?$' + if echo "$1" | _egrep_o "$pattern" >/dev/null; then + return 0 + fi + return 1 +} + +_get_record_id() { + _debug subdomain "$_sub_domain" + _debug domain "$_domain" + + if _anx_rest GET "zone.json/${_domain}/records?name=$_sub_domain&type=TXT"; then + _debug response "$response" + if _contains "$response" "\"name\":\"$_sub_domain\"" >/dev/null; then + _record_id=$(printf "%s\n" "$response" | _egrep_o "\[.\"identifier\":\"[^\"]*\"" | head -n 1 | cut -d : -f 2 | tr -d \") + else + _record_id='' + fi + else + _err "Search existing record" + fi +} + +_anx_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Content-Type: application/json" + export _H2="Authorization: Token $ANX_Token" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "${ANX_API}/$ep" "" "$m")" + else + response="$(_get "${ANX_API}/$ep")" + fi + + # shellcheck disable=SC2181 + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug response "$response" + return 0 +} + +_get_root() { + domain=$1 + i=1 + p=1 + + _anx_rest GET "zone.json" + + 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 + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done + return 1 +} diff --git a/dnsapi/dns_artfiles.sh b/dnsapi/dns_artfiles.sh new file mode 100644 index 00000000..a762837e --- /dev/null +++ b/dnsapi/dns_artfiles.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env sh + +################################################################################ +# ACME.sh 3rd party DNS API plugin for ArtFiles.de +################################################################################ +# Author: Martin Arndt, https://troublezone.net/ +# Released: 2022-02-27 +# Issues: https://github.com/acmesh-official/acme.sh/issues/4718 +################################################################################ +# Usage: +# 1. export AF_API_USERNAME='api12345678' +# 2. export AF_API_PASSWORD='apiPassword' +# 3. acme.sh --issue -d example.com --dns dns_artfiles +################################################################################ + +########## API configuration ################################################### + +AF_API_SUCCESS='status":"OK' +AF_URL_DCP='https://dcp.c.artfiles.de/api/' +AF_URL_DNS=${AF_URL_DCP}'dns/{*}_dns.html?domain=' +AF_URL_DOMAINS=${AF_URL_DCP}'domain/get_domains.html' + +########## Public functions #################################################### + +# Adds a new TXT record for given ACME challenge value & domain. +# Usage: dns_artfiles_add _acme-challenge.www.example.com "ACME challenge value" +dns_artfiles_add() { + domain="$1" + txtValue="$2" + _info 'Using ArtFiles.de DNS addition API…' + _debug 'Domain' "$domain" + _debug 'txtValue' "$txtValue" + + _set_credentials + _saveaccountconf_mutable 'AF_API_USERNAME' "$AF_API_USERNAME" + _saveaccountconf_mutable 'AF_API_PASSWORD' "$AF_API_PASSWORD" + + _set_headers + _get_zone "$domain" + _dns 'GET' + if ! _contains "$response" 'TXT'; then + _err 'Retrieving TXT records failed.' + + return 1 + fi + + _clean_records + _dns 'SET' "$(printf -- '%s\n_acme-challenge "%s"' "$response" "$txtValue")" + if ! _contains "$response" "$AF_API_SUCCESS"; then + _err 'Adding ACME challenge value failed.' + + return 1 + fi +} + +# Removes the existing TXT record for given ACME challenge value & domain. +# Usage: dns_artfiles_rm _acme-challenge.www.example.com "ACME challenge value" +dns_artfiles_rm() { + domain="$1" + txtValue="$2" + _info 'Using ArtFiles.de DNS removal API…' + _debug 'Domain' "$domain" + _debug 'txtValue' "$txtValue" + + _set_credentials + _set_headers + _get_zone "$domain" + if ! _dns 'GET'; then + return 1 + fi + + if ! _contains "$response" "$txtValue"; then + _err 'Retrieved TXT records are missing given ACME challenge value.' + + return 1 + fi + + _clean_records + response="$(printf -- '%s' "$response" | sed '/_acme-challenge "'"$txtValue"'"/d')" + _dns 'SET' "$response" + if ! _contains "$response" "$AF_API_SUCCESS"; then + _err 'Removing ACME challenge value failed.' + + return 1 + fi +} + +########## Private functions ################################################### + +# Cleans awful TXT records response of ArtFiles's API & pretty prints it. +# Usage: _clean_records +_clean_records() { + _info 'Cleaning TXT records…' + # Extract TXT part, strip trailing quote sign (ACME.sh API guidelines forbid + # usage of SED's GNU extensions, hence couldn't omit it via regex), strip '\' + # from '\"' & turn '\n' into real LF characters. + # Yup, awful API to use - but that's all we got to get this working, so… ;) + _debug2 'Raw ' "$response" + response="$(printf -- '%s' "$response" | sed 's/^.*TXT":"\([^}]*\).*$/\1/;s/,".*$//;s/.$//;s/\\"/"/g;s/\\n/\n/g')" + _debug2 'Clean' "$response" +} + +# Executes an HTTP GET or POST request for getting or setting DNS records, +# containing given payload upon POST. +# Usage: _dns [GET | SET] [payload] +_dns() { + _info 'Executing HTTP request…' + action="$1" + payload="$(printf -- '%s' "$2" | _url_encode)" + url="$(printf -- '%s%s' "$AF_URL_DNS" "$domain" | sed 's/{\*}/'"$(printf -- '%s' "$action" | _lower_case)"'/')" + + if [ "$action" = 'SET' ]; then + _debug2 'Payload' "$payload" + response="$(_post '' "$url&TXT=$payload" '' 'POST' 'application/x-www-form-urlencoded')" + else + response="$(_get "$url" '' 10)" + fi + + if ! _contains "$response" "$AF_API_SUCCESS"; then + _err "DNS API error: $response" + + return 1 + fi + + _debug 'Response' "$response" + + return 0 +} + +# Gets the root domain zone for given domain. +# Usage: _get_zone _acme-challenge.www.example.com +_get_zone() { + fqdn="$1" + domains="$(_get "$AF_URL_DOMAINS" '' 10)" + _info 'Getting domain zone…' + _debug2 'FQDN' "$fqdn" + _debug2 'Domains' "$domains" + + while _contains "$fqdn" "."; do + if _contains "$domains" "$fqdn"; then + domain="$fqdn" + _info "Found root domain zone: $domain" + break + else + fqdn="${fqdn#*.}" + _debug2 'FQDN' "$fqdn" + fi + done + + if [ "$domain" = "$fqdn" ]; then + return 0 + fi + + _err 'Couldn'\''t find root domain zone.' + + return 1 +} + +# Sets the credentials for accessing ArtFiles's API +# Usage: _set_credentials +_set_credentials() { + _info 'Setting credentials…' + AF_API_USERNAME="${AF_API_USERNAME:-$(_readaccountconf_mutable AF_API_USERNAME)}" + AF_API_PASSWORD="${AF_API_PASSWORD:-$(_readaccountconf_mutable AF_API_PASSWORD)}" + if [ -z "$AF_API_USERNAME" ] || [ -z "$AF_API_PASSWORD" ]; then + _err 'Missing ArtFiles.de username and/or password.' + _err 'Please ensure both are set via export command & try again.' + + return 1 + fi +} + +# Adds the HTTP Authorization & Content-Type headers to a follow-up request. +# Usage: _set_headers +_set_headers() { + _info 'Setting headers…' + encoded="$(printf -- '%s:%s' "$AF_API_USERNAME" "$AF_API_PASSWORD" | _base64)" + export _H1="Authorization: Basic $encoded" + export _H2='Content-Type: application/json' +} diff --git a/dnsapi/dns_arvan.sh b/dnsapi/dns_arvan.sh new file mode 100644 index 00000000..4ca5b685 --- /dev/null +++ b/dnsapi/dns_arvan.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env sh + +# Arvan_Token="Apikey xxxx" + +ARVAN_API_URL="https://napi.arvancloud.ir/cdn/4.0/domains" +# Author: Vahid Fardi +# Report Bugs here: https://github.com/Neilpang/acme.sh +# +######## Public functions ##################### + +#Usage: dns_arvan_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_arvan_add() { + fulldomain=$1 + txtvalue=$2 + _info "Using Arvan" + + Arvan_Token="${Arvan_Token:-$(_readaccountconf_mutable Arvan_Token)}" + + if [ -z "$Arvan_Token" ]; then + _err "You didn't specify \"Arvan_Token\" token yet." + _err "You can get yours from here https://npanel.arvancloud.ir/profile/api-keys" + return 1 + fi + #save the api token to the account conf file. + _saveaccountconf_mutable Arvan_Token "$Arvan_Token" + + _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 _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 duplicate"; then + _info "Already exists, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + return 0 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_arvan_rm() { + fulldomain=$1 + txtvalue=$2 + _info "Using Arvan" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + Arvan_Token="${Arvan_Token:-$(_readaccountconf_mutable Arvan_Token)}" + + _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" + _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 + + _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 ################################## + +#_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 + 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 ! _arvan_rest GET "$h"; then + return 1 + fi + 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 + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_arvan_rest() { + mtd="$1" + ep="$2" + data="$3" + + token_trimmed=$(echo "$Arvan_Token" | tr -d '"') + export _H1="Authorization: $token_trimmed" + + if [ "$mtd" = "DELETE" ]; then + #DELETE Request shouldn't have Content-Type + _debug data "$data" + response="$(_post "$data" "$ARVAN_API_URL/$ep" "" "$mtd")" + elif [ "$mtd" = "POST" ]; then + export _H2="Content-Type: application/json" + export _H3="Accept: application/json" + _debug data "$data" + response="$(_post "$data" "$ARVAN_API_URL/$ep" "" "$mtd")" + 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 2ad3c819..bb0682a7 100755 --- a/dnsapi/dns_aws.sh +++ b/dnsapi/dns_aws.sh @@ -6,11 +6,13 @@ #AWS_SECRET_ACCESS_KEY="xxxxxxx" #This is the Amazon Route53 api wrapper for acme.sh +#All `_sleep` commands are included to avoid Route53 throttling, see +#https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests AWS_HOST="route53.amazonaws.com" AWS_URL="https://$AWS_HOST" -AWS_WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-use-Amazon-Route53-API" +AWS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Amazon-Route53-API" ######## Public functions ##################### @@ -21,6 +23,7 @@ dns_aws_add() { AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-$(_readaccountconf_mutable AWS_ACCESS_KEY_ID)}" AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable AWS_SECRET_ACCESS_KEY)}" + AWS_DNS_SLOWRATE="${AWS_DNS_SLOWRATE:-$(_readaccountconf_mutable AWS_DNS_SLOWRATE)}" if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then _use_container_role || _use_instance_role @@ -29,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 @@ -38,19 +41,22 @@ dns_aws_add() { if [ -z "$_using_role" ]; then _saveaccountconf_mutable AWS_ACCESS_KEY_ID "$AWS_ACCESS_KEY_ID" _saveaccountconf_mutable AWS_SECRET_ACCESS_KEY "$AWS_SECRET_ACCESS_KEY" + _saveaccountconf_mutable AWS_DNS_SLOWRATE "$AWS_DNS_SLOWRATE" fi _debug "First detect the root zone" if ! _get_root "$fulldomain"; then _err "invalid domain" + _sleep 1 return 1 fi _debug _domain_id "$_domain_id" _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - _info "Geting existing records for $fulldomain" + _info "Getting existing records for $fulldomain" if ! aws_rest GET "2013-04-01$_domain_id/rrset" "name=$fulldomain&type=TXT"; then + _sleep 1 return 1 fi @@ -63,6 +69,7 @@ dns_aws_add() { if [ "$_resource_record" ] && _contains "$response" "$txtvalue"; then _info "The TXT record already exists. Skipping." + _sleep 1 return 0 fi @@ -72,9 +79,16 @@ dns_aws_add() { if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then _info "TXT record updated successfully." + if [ -n "$AWS_DNS_SLOWRATE" ]; then + _info "Slow rate activated: sleeping for $AWS_DNS_SLOWRATE seconds" + _sleep "$AWS_DNS_SLOWRATE" + else + _sleep 1 + fi + return 0 fi - + _sleep 1 return 1 } @@ -85,6 +99,7 @@ dns_aws_rm() { AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-$(_readaccountconf_mutable AWS_ACCESS_KEY_ID)}" AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable AWS_SECRET_ACCESS_KEY)}" + AWS_DNS_SLOWRATE="${AWS_DNS_SLOWRATE:-$(_readaccountconf_mutable AWS_DNS_SLOWRATE)}" if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ]; then _use_container_role || _use_instance_role @@ -93,6 +108,7 @@ dns_aws_rm() { _debug "First detect the root zone" if ! _get_root "$fulldomain"; then _err "invalid domain" + _sleep 1 return 1 fi _debug _domain_id "$_domain_id" @@ -101,6 +117,7 @@ dns_aws_rm() { _info "Getting existing records for $fulldomain" if ! aws_rest GET "2013-04-01$_domain_id/rrset" "name=$fulldomain&type=TXT"; then + _sleep 1 return 1 fi @@ -109,6 +126,7 @@ dns_aws_rm() { _debug "_resource_record" "$_resource_record" else _debug "no records exist, skip" + _sleep 1 return 0 fi @@ -116,9 +134,16 @@ dns_aws_rm() { if aws_rest POST "2013-04-01$_domain_id/rrset/" "" "$_aws_tmpl_xml" && _contains "$response" "ChangeResourceRecordSetsResponse"; then _info "TXT record deleted successfully." + if [ -n "$AWS_DNS_SLOWRATE" ]; then + _info "Slow rate activated: sleeping for $AWS_DNS_SLOWRATE seconds" + _sleep "$AWS_DNS_SLOWRATE" + else + _sleep 1 + fi + return 0 fi - + _sleep 1 return 1 } @@ -127,34 +152,23 @@ dns_aws_rm() { _get_root() { domain=$1 - i=2 + i=1 p=1 - if aws_rest GET "2013-04-01/hostedzone"; then - while true; do - h=$(printf "%s" "$domain" | cut -d . -f $i-100) - _debug2 "Checking domain: $h" - if [ -z "$h" ]; then - if _contains "$response" "true" && _contains "$response" ""; then - _debug "IsTruncated" - _nextMarker="$(echo "$response" | _egrep_o ".*" | cut -d '>' -f 2 | cut -d '<' -f 1)" - _debug "NextMarker" "$_nextMarker" - if aws_rest GET "2013-04-01/hostedzone" "marker=$_nextMarker"; then - _debug "Truncated request OK" - i=2 - p=1 - continue - else - _err "Truncated request error." - fi - fi - #not valid - _err "Invalid domain" - return 1 - fi + # iterate over names (a.b.c.d -> b.c.d -> c.d -> d) + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100 | sed 's/\./\\./g') + _debug "Checking domain: $h" + if [ -z "$h" ]; then + _error "invalid domain" + return 1 + fi + # iterate over paginated result for list_hosted_zones + aws_rest GET "2013-04-01/hostedzone" + while true; do if _contains "$response" "$h."; then - hostedzone="$(echo "$response" | sed 's//#&/g' | tr '#' '\n' | _egrep_o "[^<]*<.Id>$h.<.Name>.*false<.PrivateZone>.*<.HostedZone>")" + hostedzone="$(echo "$response" | tr -d '\n' | sed 's//#&/g' | tr '#' '\n' | _egrep_o "[^<]*<.Id>$h.<.Name>.*false<.PrivateZone>.*<.HostedZone>")" _debug hostedzone "$hostedzone" if [ "$hostedzone" ]; then _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o ".*<.Id>" | head -n 1 | _egrep_o ">.*<" | tr -d "<>") @@ -167,10 +181,19 @@ _get_root() { return 1 fi fi - p=$i - i=$(_math "$i" + 1) + if _contains "$response" "true" && _contains "$response" ""; then + _debug "IsTruncated" + _nextMarker="$(echo "$response" | _egrep_o ".*" | cut -d '>' -f 2 | cut -d '<' -f 1)" + _debug "NextMarker" "$_nextMarker" + else + break + fi + _debug "Checking domain: $h - Next Page " + aws_rest GET "2013-04-01/hostedzone" "marker=$_nextMarker" done - fi + p=$i + i=$(_math "$i" + 1) + done return 1 } @@ -197,21 +220,21 @@ _use_instance_role() { _use_metadata() { _aws_creds="$( - _get "$1" "" 1 \ - | _normalizeJson \ - | tr '{,}' '\n' \ - | while read -r _line; do + _get "$1" "" 1 | + _normalizeJson | + tr '{,}' '\n' | + while read -r _line; do _key="$(echo "${_line%%:*}" | tr -d '"')" _value="${_line#*:}" _debug3 "_key" "$_key" _secure_debug3 "_value" "$_value" case "$_key" in - AccessKeyId) echo "AWS_ACCESS_KEY_ID=$_value" ;; - SecretAccessKey) echo "AWS_SECRET_ACCESS_KEY=$_value" ;; - Token) echo "AWS_SESSION_TOKEN=$_value" ;; + AccessKeyId) echo "AWS_ACCESS_KEY_ID=$_value" ;; + SecretAccessKey) echo "AWS_SECRET_ACCESS_KEY=$_value" ;; + Token) echo "AWS_SESSION_TOKEN=$_value" ;; esac - done \ - | paste -sd' ' - + done | + paste -sd' ' - )" _secure_debug "_aws_creds" "$_aws_creds" 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_azure.sh b/dnsapi/dns_azure.sh index c6893a0c..1c33c13a 100644 --- a/dnsapi/dns_azure.sh +++ b/dnsapi/dns_azure.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-use-Azure-DNS" +WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS" ######## Public functions ##################### @@ -9,57 +9,72 @@ WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-use-Azure-DNS" # # Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/createorupdate # + dns_azure_add() { fulldomain=$1 txtvalue=$2 AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}" - AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" - AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" - AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" - if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then AZUREDNS_SUBSCRIPTIONID="" AZUREDNS_TENANTID="" AZUREDNS_APPID="" AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure Subscription ID " + _err "You didn't specify the Azure Subscription ID" return 1 fi - - if [ -z "$AZUREDNS_TENANTID" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure Tenant ID " - return 1 - fi - - if [ -z "$AZUREDNS_APPID" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure App ID" - return 1 - fi - - if [ -z "$AZUREDNS_CLIENTSECRET" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure Client Secret" - return 1 - fi - #save account details to account conf file. + #save subscription id to account conf file. _saveaccountconf_mutable AZUREDNS_SUBSCRIPTIONID "$AZUREDNS_SUBSCRIPTIONID" - _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID" - _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID" - _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET" - accesstoken=$(_azure_getaccess_token "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") + AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}" + if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then + _info "Using Azure managed identity" + #save managed identity as preferred authentication method, clear service principal credentials from conf file. + _saveaccountconf_mutable AZUREDNS_MANAGEDIDENTITY "$AZUREDNS_MANAGEDIDENTITY" + _saveaccountconf_mutable AZUREDNS_TENANTID "" + _saveaccountconf_mutable AZUREDNS_APPID "" + _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "" + else + _info "You didn't ask to use Azure managed identity, checking service principal credentials" + AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" + AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" + AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" + + if [ -z "$AZUREDNS_TENANTID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure Tenant ID " + return 1 + fi + + if [ -z "$AZUREDNS_APPID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure App ID" + return 1 + fi + + if [ -z "$AZUREDNS_CLIENTSECRET" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure Client Secret" + return 1 + fi + + #save account details to account conf file, don't opt in for azure manages identity check. + _saveaccountconf_mutable AZUREDNS_MANAGEDIDENTITY "false" + _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID" + _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID" + _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET" + fi + + accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then _err "invalid domain" @@ -116,10 +131,6 @@ dns_azure_rm() { txtvalue=$2 AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}" - AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" - AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" - AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" - if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then AZUREDNS_SUBSCRIPTIONID="" AZUREDNS_TENANTID="" @@ -129,34 +140,44 @@ dns_azure_rm() { return 1 fi - if [ -z "$AZUREDNS_TENANTID" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure Tenant ID " - return 1 + AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}" + if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then + _info "Using Azure managed identity" + else + _info "You didn't ask to use Azure managed identity, checking service principal credentials" + AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" + AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" + AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" + + if [ -z "$AZUREDNS_TENANTID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure Tenant ID " + return 1 + fi + + if [ -z "$AZUREDNS_APPID" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure App ID" + return 1 + fi + + if [ -z "$AZUREDNS_CLIENTSECRET" ]; then + AZUREDNS_SUBSCRIPTIONID="" + AZUREDNS_TENANTID="" + AZUREDNS_APPID="" + AZUREDNS_CLIENTSECRET="" + _err "You didn't specify the Azure Client Secret" + return 1 + fi fi - if [ -z "$AZUREDNS_APPID" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure App ID" - return 1 - fi - - if [ -z "$AZUREDNS_CLIENTSECRET" ]; then - AZUREDNS_SUBSCRIPTIONID="" - AZUREDNS_TENANTID="" - AZUREDNS_APPID="" - AZUREDNS_CLIENTSECRET="" - _err "You didn't specify the Azure Client Secret" - return 1 - fi - - accesstoken=$(_azure_getaccess_token "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") + accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then _err "invalid domain" @@ -172,7 +193,7 @@ dns_azure_rm() { _azure_rest GET "$acmeRecordURI" "" "$accesstoken" timestamp="$(_time)" if [ "$_code" = "200" ]; then - vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v "$txtvalue")" + vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v -- "$txtvalue")" values="" comma="" for v in $vlist; do @@ -220,7 +241,7 @@ _azure_rest() { export _H2="accept: application/json" export _H3="Content-Type: application/json" # clear headers from previous request to avoid getting wrong http code on timeouts - :>"$HTTP_HEADER" + : >"$HTTP_HEADER" _debug "$ep" if [ "$m" != "GET" ]; then _secure_debug2 "data $data" @@ -258,9 +279,10 @@ _azure_rest() { ## Ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-service-to-service#request-an-access-token _azure_getaccess_token() { - tenantID=$1 - clientID=$2 - clientSecret=$3 + managedIdentity=$1 + tenantID=$2 + clientID=$3 + clientSecret=$4 accesstoken="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}" expires_on="${AZUREDNS_TOKENVALIDTO:-$(_readaccountconf_mutable AZUREDNS_TOKENVALIDTO)}" @@ -278,17 +300,25 @@ _azure_getaccess_token() { fi _debug "getting new bearer token" - export _H1="accept: application/json" - export _H2="Content-Type: application/x-www-form-urlencoded" - - body="resource=$(printf "%s" 'https://management.core.windows.net/' | _url_encode)&client_id=$(printf "%s" "$clientID" | _url_encode)&client_secret=$(printf "%s" "$clientSecret" | _url_encode)&grant_type=client_credentials" - _secure_debug2 "data $body" - response="$(_post "$body" "https://login.microsoftonline.com/$tenantID/oauth2/token" "" "POST")" - _ret="$?" - _secure_debug2 "response $response" - response="$(echo "$response" | _normalizeJson)" - accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") - expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + if [ "$managedIdentity" = true ]; then + # https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http + export _H1="Metadata: true" + response="$(_get http://169.254.169.254/metadata/identity/oauth2/token\?api-version=2018-02-01\&resource=https://management.azure.com/)" + response="$(echo "$response" | _normalizeJson)" + accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + else + export _H1="accept: application/json" + export _H2="Content-Type: application/x-www-form-urlencoded" + body="resource=$(printf "%s" 'https://management.core.windows.net/' | _url_encode)&client_id=$(printf "%s" "$clientID" | _url_encode)&client_secret=$(printf "%s" "$clientSecret" | _url_encode)&grant_type=client_credentials" + _secure_debug2 "data $body" + response="$(_post "$body" "https://login.microsoftonline.com/$tenantID/oauth2/token" "" "POST")" + _ret="$?" + _secure_debug2 "response $response" + response="$(echo "$response" | _normalizeJson)" + accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + fi if [ -z "$accesstoken" ]; then _err "no acccess token received. Check your Azure settings see $WIKI" @@ -316,8 +346,8 @@ _get_root() { ## (ZoneListResult with continuation token for the next page of results) ## Per https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#dns-limits you are limited to 100 Zone/subscriptions anyways ## - _azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?api-version=2017-09-01" "" "$accesstoken" - # Find matching domain name is Json response + _azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?\$top=500&api-version=2017-09-01" "" "$accesstoken" + # Find matching domain name in Json response while true; do h=$(printf "%s" "$domain" | cut -d . -f $i-100) _debug2 "Checking domain: $h" @@ -328,7 +358,7 @@ _get_root() { fi if _contains "$response" "\"name\":\"$h\"" >/dev/null; then - _domain_id=$(echo "$response" | _egrep_o "\\{\"id\":\"[^\"]*$h\"" | head -n 1 | cut -d : -f 2 | tr -d \") + _domain_id=$(echo "$response" | _egrep_o "\\{\"id\":\"[^\"]*\\/$h\"" | head -n 1 | cut -d : -f 2 | tr -d \") if [ "$_domain_id" ]; then if [ "$i" = 1 ]; then #create the record at the domain apex (@) if only the domain name was provided as --domain-alias diff --git a/dnsapi/dns_bookmyname.sh b/dnsapi/dns_bookmyname.sh new file mode 100644 index 00000000..62548fd0 --- /dev/null +++ b/dnsapi/dns_bookmyname.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env sh + +#Here is a sample custom api script. +#This file name is "dns_bookmyname.sh" +#So, here must be a method dns_bookmyname_add() +#Which will be called by acme.sh to add the txt record to your api system. +#returns 0 means success, otherwise error. +# +#Author: Neilpang +#Report Bugs here: https://github.com/acmesh-official/acme.sh +# +######## Public functions ##################### + +# Please Read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide + +# BookMyName urls: +# https://BOOKMYNAME_USERNAME:BOOKMYNAME_PASSWORD@www.bookmyname.com/dyndns/?hostname=_acme-challenge.domain.tld&type=txt&ttl=300&do=add&value="XXXXXXXX"' +# https://BOOKMYNAME_USERNAME:BOOKMYNAME_PASSWORD@www.bookmyname.com/dyndns/?hostname=_acme-challenge.domain.tld&type=txt&ttl=300&do=remove&value="XXXXXXXX"' + +# Output: +#good: update done, cid 123456, domain id 456789, type txt, ip XXXXXXXX +#good: remove done 1, cid 123456, domain id 456789, ttl 300, type txt, ip XXXXXXXX + +# Be careful, BMN DNS servers can be slow to pick up changes; using dnssleep is thus advised. + +# Usage: +# export BOOKMYNAME_USERNAME="ABCDE-FREE" +# export BOOKMYNAME_PASSWORD="MyPassword" +# /usr/local/ssl/acme.sh/acme.sh --dns dns_bookmyname --dnssleep 600 --issue -d domain.tld + +#Usage: dns_bookmyname_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_bookmyname_add() { + fulldomain=$1 + txtvalue=$2 + _info "Using bookmyname" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + BOOKMYNAME_USERNAME="${BOOKMYNAME_USERNAME:-$(_readaccountconf_mutable BOOKMYNAME_USERNAME)}" + BOOKMYNAME_PASSWORD="${BOOKMYNAME_PASSWORD:-$(_readaccountconf_mutable BOOKMYNAME_PASSWORD)}" + + if [ -z "$BOOKMYNAME_USERNAME" ] || [ -z "$BOOKMYNAME_PASSWORD" ]; then + BOOKMYNAME_USERNAME="" + BOOKMYNAME_PASSWORD="" + _err "You didn't specify BookMyName username and password yet." + _err "Please specify them and try again." + return 1 + fi + + #save the credentials to the account conf file. + _saveaccountconf_mutable BOOKMYNAME_USERNAME "$BOOKMYNAME_USERNAME" + _saveaccountconf_mutable BOOKMYNAME_PASSWORD "$BOOKMYNAME_PASSWORD" + + uri="https://${BOOKMYNAME_USERNAME}:${BOOKMYNAME_PASSWORD}@www.bookmyname.com/dyndns/" + data="?hostname=${fulldomain}&type=TXT&ttl=300&do=add&value=${txtvalue}" + result="$(_get "${uri}${data}")" + _debug "Result: $result" + + if ! _startswith "$result" 'good: update done, cid '; then + _err "Can't add $fulldomain" + return 1 + fi + +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_bookmyname_rm() { + fulldomain=$1 + txtvalue=$2 + _info "Using bookmyname" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + BOOKMYNAME_USERNAME="${BOOKMYNAME_USERNAME:-$(_readaccountconf_mutable BOOKMYNAME_USERNAME)}" + BOOKMYNAME_PASSWORD="${BOOKMYNAME_PASSWORD:-$(_readaccountconf_mutable BOOKMYNAME_PASSWORD)}" + + uri="https://${BOOKMYNAME_USERNAME}:${BOOKMYNAME_PASSWORD}@www.bookmyname.com/dyndns/" + data="?hostname=${fulldomain}&type=TXT&ttl=300&do=remove&value=${txtvalue}" + result="$(_get "${uri}${data}")" + _debug "Result: $result" + + if ! _startswith "$result" 'good: remove done 1, cid '; then + _info "Can't remove $fulldomain" + fi + +} + +#################### Private functions below ################################## diff --git a/dnsapi/dns_bunny.sh b/dnsapi/dns_bunny.sh new file mode 100644 index 00000000..a9b1ea5a --- /dev/null +++ b/dnsapi/dns_bunny.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env sh + +## Will be called by acme.sh to add the TXT record via the Bunny DNS API. +## returns 0 means success, otherwise error. + +## Author: nosilver4u +## GitHub: https://github.com/nosilver4u/acme.sh + +## +## Environment Variables Required: +## +## BUNNY_API_KEY="75310dc4-ca77-9ac3-9a19-f6355db573b49ce92ae1-2655-3ebd-61ac-3a3ae34834cc" +## + +##################### Public functions ##################### + +## Create the text record for validation. +## Usage: fulldomain txtvalue +## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs" +dns_bunny_add() { + fulldomain="$(echo "$1" | _lower_case)" + txtvalue=$2 + + BUNNY_API_KEY="${BUNNY_API_KEY:-$(_readaccountconf_mutable BUNNY_API_KEY)}" + # Check if API Key is set + if [ -z "$BUNNY_API_KEY" ]; then + BUNNY_API_KEY="" + _err "You did not specify Bunny.net API key." + _err "Please export BUNNY_API_KEY and try again." + return 1 + fi + + _info "Using Bunny.net dns validation - add record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + ## save the env vars (key and domain split location) for later automated use + _saveaccountconf_mutable BUNNY_API_KEY "$BUNNY_API_KEY" + + ## split the domain for Bunny API + if ! _get_base_domain "$fulldomain"; then + _err "domain not found in your account for addition" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug _domain_id "$_domain_id" + + ## Set the header with our post type and auth key + export _H1="Accept: application/json" + export _H2="AccessKey: $BUNNY_API_KEY" + export _H3="Content-Type: application/json" + PURL="https://api.bunny.net/dnszone/$_domain_id/records" + PBODY='{"Id":'$_domain_id',"Type":3,"Name":"'$_sub_domain'","Value":"'$txtvalue'","ttl":120}' + + _debug PURL "$PURL" + _debug PBODY "$PBODY" + + ## the create request - POST + ## args: BODY, URL, [need64, httpmethod] + response="$(_post "$PBODY" "$PURL" "" "PUT")" + + ## check response + if [ "$?" != "0" ]; then + _err "error in response: $response" + return 1 + fi + _debug2 response "$response" + + ## finished correctly + return 0 +} + +## Remove the txt record after validation. +## Usage: fulldomain txtvalue +## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs" +dns_bunny_rm() { + fulldomain="$(echo "$1" | _lower_case)" + txtvalue=$2 + + BUNNY_API_KEY="${BUNNY_API_KEY:-$(_readaccountconf_mutable BUNNY_API_KEY)}" + # Check if API Key Exists + if [ -z "$BUNNY_API_KEY" ]; then + BUNNY_API_KEY="" + _err "You did not specify Bunny.net API key." + _err "Please export BUNNY_API_KEY and try again." + return 1 + fi + + _info "Using Bunny.net dns validation - remove record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + ## split the domain for Bunny API + if ! _get_base_domain "$fulldomain"; then + _err "Domain not found in your account for TXT record removal" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug _domain_id "$_domain_id" + + ## Set the header with our post type and key auth key + export _H1="Accept: application/json" + export _H2="AccessKey: $BUNNY_API_KEY" + ## get URL for the list of DNS records + GURL="https://api.bunny.net/dnszone/$_domain_id" + + ## 1) Get the domain/zone records + ## the fetch request - GET + ## args: URL, [onlyheader, timeout] + domain_list="$(_get "$GURL")" + + ## check response + if [ "$?" != "0" ]; then + _err "error in domain_list response: $domain_list" + return 1 + fi + _debug2 domain_list "$domain_list" + + ## 2) search through records + ## check for what we are looking for: "Type":3,"Value":"$txtvalue","Name":"$_sub_domain" + record="$(echo "$domain_list" | _egrep_o "\"Id\"\s*\:\s*\"*[0-9]+\"*,\s*\"Type\"[^}]*\"Value\"\s*\:\s*\"$txtvalue\"[^}]*\"Name\"\s*\:\s*\"$_sub_domain\"")" + + if [ -n "$record" ]; then + + ## We found records + rec_ids="$(echo "$record" | _egrep_o "Id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")" + _debug rec_ids "$rec_ids" + if [ -n "$rec_ids" ]; then + echo "$rec_ids" | while IFS= read -r rec_id; do + ## delete the record + ## delete URL for removing the one we dont want + DURL="https://api.bunny.net/dnszone/$_domain_id/records/$rec_id" + + ## the removal request - DELETE + ## args: BODY, URL, [need64, httpmethod] + response="$(_post "" "$DURL" "" "DELETE")" + + ## check response (sort of) + if [ "$?" != "0" ]; then + _err "error in remove response: $response" + return 1 + fi + _debug2 response "$response" + + done + fi + fi + + ## finished correctly + return 0 +} + +##################### Private functions below ##################### + +## Split the domain provided into the "base domain" and the "start prefix". +## This function searches for the longest subdomain in your account +## for the full domain given and splits it into the base domain (zone) +## and the prefix/record to be added/removed +## USAGE: fulldomain +## EG: "_acme-challenge.two.three.four.domain.com" +## returns +## _sub_domain="_acme-challenge.two" +## _domain="three.four.domain.com" *IF* zone "three.four.domain.com" exists +## _domain_id=234 +## if only "domain.com" exists it will return +## _sub_domain="_acme-challenge.two.three.four" +## _domain="domain.com" +## _domain_id=234 +_get_base_domain() { + # args + fulldomain="$(echo "$1" | _lower_case)" + _debug fulldomain "$fulldomain" + + # domain max legal length = 253 + MAX_DOM=255 + page=1 + + ## get a list of domains for the account to check thru + ## Set the headers + export _H1="Accept: application/json" + export _H2="AccessKey: $BUNNY_API_KEY" + _debug BUNNY_API_KEY "$BUNNY_API_KEY" + ## get URL for the list of domains + ## may get: "links":{"pages":{"last":".../v2/domains/DOM/records?page=2","next":".../v2/domains/DOM/records?page=2"}} + DOMURL="https://api.bunny.net/dnszone" + + ## while we dont have a matching domain we keep going + while [ -z "$found" ]; do + ## get the domain list (current page) + domain_list="$(_get "$DOMURL")" + + ## check response + if [ "$?" != "0" ]; then + _err "error in domain_list response: $domain_list" + return 1 + fi + _debug2 domain_list "$domain_list" + + i=1 + while [ $i -gt 0 ]; do + ## get next longest domain + _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM") + ## check we got something back from our cut (or are we at the end) + if [ -z "$_domain" ]; then + break + fi + ## we got part of a domain back - grep it out + found="$(echo "$domain_list" | _egrep_o "\"Id\"\s*:\s*\"*[0-9]+\"*,\s*\"Domain\"\s*\:\s*\"$_domain\"")" + ## check if it exists + if [ -n "$found" ]; then + ## exists - exit loop returning the parts + sub_point=$(_math $i - 1) + _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point") + _domain_id="$(echo "$found" | _egrep_o "Id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")" + _debug _domain_id "$_domain_id" + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + found="" + return 0 + fi + ## increment cut point $i + i=$(_math $i + 1) + done + + if [ -z "$found" ]; then + page=$(_math $page + 1) + nextpage="https://api.bunny.net/dnszone?page=$page" + ## Find the next page if we don't have a match. + hasnextpage="$(echo "$domain_list" | _egrep_o "\"HasMoreItems\"\s*:\s*true")" + if [ -z "$hasnextpage" ]; then + _err "No record and no nextpage in Bunny.net domain search." + found="" + return 1 + fi + _debug2 nextpage "$nextpage" + DOMURL="$nextpage" + fi + + done + + ## We went through the entire domain zone list and didn't find one that matched. + ## If we ever get here, something is broken in the code... + _err "Domain not found in Bunny.net account, but we should never get here!" + found="" + return 1 +} diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh index 3595b9b0..cd8d9a8d 100755 --- a/dnsapi/dns_cf.sh +++ b/dnsapi/dns_cf.sh @@ -5,6 +5,10 @@ # #CF_Email="xxxx@sss.com" +#CF_Token="xxxx" +#CF_Account_ID="xxxx" +#CF_Zone_ID="xxxx" + CF_Api="https://api.cloudflare.com/client/v4" ######## Public functions ##################### @@ -14,25 +18,49 @@ dns_cf_add() { fulldomain=$1 txtvalue=$2 + CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}" + CF_Account_ID="${CF_Account_ID:-$(_readaccountconf_mutable CF_Account_ID)}" + CF_Zone_ID="${CF_Zone_ID:-$(_readaccountconf_mutable CF_Zone_ID)}" CF_Key="${CF_Key:-$(_readaccountconf_mutable CF_Key)}" CF_Email="${CF_Email:-$(_readaccountconf_mutable CF_Email)}" - if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then - CF_Key="" - CF_Email="" - _err "You didn't specify a cloudflare api key and email yet." - _err "Please create the key and try again." - return 1 - fi - if ! _contains "$CF_Email" "@"; then - _err "It seems that the CF_Email=$CF_Email is not a valid email address." - _err "Please check and retry." - return 1 - fi + if [ "$CF_Token" ]; then + if [ "$CF_Zone_ID" ]; then + _savedomainconf CF_Token "$CF_Token" + _savedomainconf CF_Account_ID "$CF_Account_ID" + _savedomainconf CF_Zone_ID "$CF_Zone_ID" + else + _saveaccountconf_mutable CF_Token "$CF_Token" + _saveaccountconf_mutable CF_Account_ID "$CF_Account_ID" + _clearaccountconf_mutable CF_Zone_ID + _clearaccountconf CF_Zone_ID + fi + else + if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then + CF_Key="" + CF_Email="" + _err "You didn't specify a Cloudflare api key and email yet." + _err "You can get yours from here https://dash.cloudflare.com/profile." + return 1 + fi - #save the api key and email to the account conf file. - _saveaccountconf_mutable CF_Key "$CF_Key" - _saveaccountconf_mutable CF_Email "$CF_Email" + if ! _contains "$CF_Email" "@"; then + _err "It seems that the CF_Email=$CF_Email is not a valid email address." + _err "Please check and retry." + return 1 + fi + #save the api key and email to the account conf file. + _saveaccountconf_mutable CF_Key "$CF_Key" + _saveaccountconf_mutable CF_Email "$CF_Email" + + _clearaccountconf_mutable CF_Token + _clearaccountconf_mutable CF_Account_ID + _clearaccountconf_mutable CF_Zone_ID + _clearaccountconf CF_Token + _clearaccountconf CF_Account_ID + _clearaccountconf CF_Zone_ID + + fi _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -46,7 +74,7 @@ dns_cf_add() { _debug "Getting txt records" _cf_rest GET "zones/${_domain_id}/dns_records?type=TXT&name=$fulldomain" - if ! printf "%s" "$response" | grep \"success\":true >/dev/null; then + if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then _err "Error" return 1 fi @@ -58,9 +86,12 @@ dns_cf_add() { # if [ "$count" = "0" ]; then _info "Adding record" if _cf_rest POST "zones/$_domain_id/dns_records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then - if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; 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 @@ -68,19 +99,6 @@ dns_cf_add() { fi _err "Add txt record error." return 1 - # else - # _info "Updating record" - # record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1) - # _debug "record_id" "$record_id" - # - # _cf_rest PUT "zones/$_domain_id/dns_records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"zone_name\":\"$_domain\"}" - # if [ "$?" = "0" ]; then - # _info "Updated, OK" - # return 0 - # fi - # _err "Update error" - # return 1 - # fi } @@ -89,15 +107,11 @@ dns_cf_rm() { fulldomain=$1 txtvalue=$2 + CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}" + CF_Account_ID="${CF_Account_ID:-$(_readaccountconf_mutable CF_Account_ID)}" + CF_Zone_ID="${CF_Zone_ID:-$(_readaccountconf_mutable CF_Zone_ID)}" CF_Key="${CF_Key:-$(_readaccountconf_mutable CF_Key)}" CF_Email="${CF_Email:-$(_readaccountconf_mutable CF_Email)}" - if [ -z "$CF_Key" ] || [ -z "$CF_Email" ]; then - CF_Key="" - CF_Email="" - _err "You didn't specify a cloudflare api key and email yet." - _err "Please create the key and try again." - return 1 - fi _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -111,17 +125,17 @@ dns_cf_rm() { _debug "Getting txt records" _cf_rest GET "zones/${_domain_id}/dns_records?type=TXT&name=$fulldomain&content=$txtvalue" - if ! printf "%s" "$response" | grep \"success\":true >/dev/null; then - _err "Error" + if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then + _err "Error: $response" return 1 fi - count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) + 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=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1) + record_id=$(echo "$response" | _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." @@ -131,7 +145,7 @@ dns_cf_rm() { _err "Delete record error." return 1 fi - _contains "$response" '"success":true' + echo "$response" | tr -d " " | grep \"success\":true >/dev/null fi } @@ -144,8 +158,30 @@ dns_cf_rm() { # _domain_id=sdjkglgdfewsdfg _get_root() { domain=$1 - i=2 + i=1 p=1 + + # Use Zone ID directly if provided + if [ "$CF_Zone_ID" ]; then + if ! _cf_rest GET "zones/$CF_Zone_ID"; then + return 1 + else + if echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then + _domain=$(echo "$response" | _egrep_o "\"name\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") + if [ "$_domain" ]; then + _cutlength=$((${#domain} - ${#_domain} - 1)) + _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength") + _domain_id=$CF_Zone_ID + return 0 + else + return 1 + fi + else + return 1 + fi + fi + fi + while true; do h=$(printf "%s" "$domain" | cut -d . -f $i-100) _debug h "$h" @@ -154,12 +190,18 @@ _get_root() { return 1 fi - if ! _cf_rest GET "zones?name=$h"; then - return 1 + if [ "$CF_Account_ID" ]; then + if ! _cf_rest GET "zones?name=$h&account.id=$CF_Account_ID"; then + return 1 + fi + else + if ! _cf_rest GET "zones?name=$h"; then + return 1 + fi fi - if _contains "$response" "\"name\":\"$h\"" >/dev/null; then - _domain_id=$(printf "%s\n" "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | head -n 1 | cut -d : -f 2 | tr -d \") + if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_count":1'; then + _domain_id=$(echo "$response" | _egrep_o "\[.\"id\": *\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d " ") if [ "$_domain_id" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) _domain=$h @@ -179,9 +221,17 @@ _cf_rest() { data="$3" _debug "$ep" - export _H1="X-Auth-Email: $CF_Email" - export _H2="X-Auth-Key: $CF_Key" - export _H3="Content-Type: application/json" + email_trimmed=$(echo "$CF_Email" | tr -d '"') + key_trimmed=$(echo "$CF_Key" | tr -d '"') + token_trimmed=$(echo "$CF_Token" | tr -d '"') + + export _H1="Content-Type: application/json" + if [ "$token_trimmed" ]; then + export _H2="Authorization: Bearer $token_trimmed" + else + export _H2="X-Auth-Email: $email_trimmed" + export _H3="X-Auth-Key: $key_trimmed" + fi if [ "$m" != "GET" ]; then _debug data "$data" diff --git a/dnsapi/dns_clouddns.sh b/dnsapi/dns_clouddns.sh new file mode 100755 index 00000000..31ae4ee9 --- /dev/null +++ b/dnsapi/dns_clouddns.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env sh + +# Author: Radek Sprta + +#CLOUDDNS_EMAIL=XXXXX +#CLOUDDNS_PASSWORD="YYYYYYYYY" +#CLOUDDNS_CLIENT_ID=XXXXX + +CLOUDDNS_API='https://admin.vshosting.cloud/clouddns' +CLOUDDNS_LOGIN_API='https://admin.vshosting.cloud/api/public/auth/login' + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_clouddns_add() { + fulldomain=$1 + txtvalue=$2 + _debug "fulldomain" "$fulldomain" + + CLOUDDNS_CLIENT_ID="${CLOUDDNS_CLIENT_ID:-$(_readaccountconf_mutable CLOUDDNS_CLIENT_ID)}" + CLOUDDNS_EMAIL="${CLOUDDNS_EMAIL:-$(_readaccountconf_mutable CLOUDDNS_EMAIL)}" + CLOUDDNS_PASSWORD="${CLOUDDNS_PASSWORD:-$(_readaccountconf_mutable CLOUDDNS_PASSWORD)}" + + if [ -z "$CLOUDDNS_PASSWORD" ] || [ -z "$CLOUDDNS_EMAIL" ] || [ -z "$CLOUDDNS_CLIENT_ID" ]; then + CLOUDDNS_CLIENT_ID="" + CLOUDDNS_EMAIL="" + CLOUDDNS_PASSWORD="" + _err "You didn't specify a CloudDNS password, email and client ID yet." + return 1 + fi + if ! _contains "$CLOUDDNS_EMAIL" "@"; then + _err "It seems that the CLOUDDNS_EMAIL=$CLOUDDNS_EMAIL is not a valid email address." + _err "Please check and retry." + return 1 + fi + # Save CloudDNS client id, email and password to config file + _saveaccountconf_mutable CLOUDDNS_CLIENT_ID "$CLOUDDNS_CLIENT_ID" + _saveaccountconf_mutable CLOUDDNS_EMAIL "$CLOUDDNS_EMAIL" + _saveaccountconf_mutable CLOUDDNS_PASSWORD "$CLOUDDNS_PASSWORD" + + _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" + + # Add TXT record + data="{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"value\":\"$txtvalue\",\"domainId\":\"$_domain_id\"}" + if _clouddns_api POST "record-txt" "$data"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + elif _contains "$response" '"code":4136'; then + _info "Already exists, OK" + else + _err "Add TXT record error." + return 1 + fi + fi + + _debug "Publishing record changes" + _clouddns_api PUT "domain/$_domain_id/publish" "{\"soaTtl\":300}" +} + +# Usage: rm _acme-challenge.www.domain.com +dns_clouddns_rm() { + fulldomain=$1 + _debug "fulldomain" "$fulldomain" + + CLOUDDNS_CLIENT_ID="${CLOUDDNS_CLIENT_ID:-$(_readaccountconf_mutable CLOUDDNS_CLIENT_ID)}" + CLOUDDNS_EMAIL="${CLOUDDNS_EMAIL:-$(_readaccountconf_mutable CLOUDDNS_EMAIL)}" + CLOUDDNS_PASSWORD="${CLOUDDNS_PASSWORD:-$(_readaccountconf_mutable CLOUDDNS_PASSWORD)}" + + _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" + + # Get record ID + _clouddns_api GET "domain/$_domain_id" + if _contains "$response" "lastDomainRecordList"; then + re="\"lastDomainRecordList\".*\"id\":\"([^\"}]*)\"[^}]*\"name\":\"$fulldomain.\"," + _last_domains=$(echo "$response" | _egrep_o "$re") + re2="\"id\":\"([^\"}]*)\"[^}]*\"name\":\"$fulldomain.\"," + _record_id=$(echo "$_last_domains" | _egrep_o "$re2" | _head_n 1 | cut -d : -f 2 | cut -d , -f 1 | tr -d "\"") + _debug _record_id "$_record_id" + else + _err "Could not retrieve record ID" + return 1 + fi + + _info "Removing record" + if _clouddns_api DELETE "record/$_record_id"; then + if _contains "$response" "\"error\":"; then + _err "Could not remove record" + return 1 + fi + fi + + _debug "Publishing record changes" + _clouddns_api PUT "domain/$_domain_id/publish" "{\"soaTtl\":300}" +} + +#################### Private functions below ################################## + +# Usage: _get_root _acme-challenge.www.domain.com +# Returns: +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + + # Get domain root + data="{\"search\": [{\"name\": \"clientId\", \"operator\": \"eq\", \"value\": \"$CLOUDDNS_CLIENT_ID\"}]}" + _clouddns_api "POST" "domain/search" "$data" + domain_slice="$domain" + while [ -z "$domain_root" ]; do + if _contains "$response" "\"domainName\":\"$domain_slice\.\""; then + domain_root="$domain_slice" + _debug domain_root "$domain_root" + fi + domain_slice="$(echo "$domain_slice" | cut -d . -f 2-)" + done + + # Get domain id + data="{\"search\": [{\"name\": \"clientId\", \"operator\": \"eq\", \"value\": \"$CLOUDDNS_CLIENT_ID\"}, \ + {\"name\": \"domainName\", \"operator\": \"eq\", \"value\": \"$domain_root.\"}]}" + _clouddns_api "POST" "domain/search" "$data" + if _contains "$response" "\"id\":\""; then + re='domainType\":\"[^\"]*\",\"id\":\"([^\"]*)\",' # Match domain id + _domain_id=$(echo "$response" | _egrep_o "$re" | _head_n 1 | cut -d : -f 3 | tr -d "\",") + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | sed "s/.$domain_root//") + _domain="$domain_root" + return 0 + fi + _err 'Domain name not found on your CloudDNS account' + return 1 + fi + return 1 +} + +# Usage: _clouddns_api GET domain/search '{"data": "value"}' +# Returns: +# response='{"message": "api response"}' +_clouddns_api() { + method=$1 + endpoint="$2" + data="$3" + _debug endpoint "$endpoint" + + if [ -z "$CLOUDDNS_TOKEN" ]; then + _clouddns_login + fi + _debug CLOUDDNS_TOKEN "$CLOUDDNS_TOKEN" + + export _H1="Content-Type: application/json" + export _H2="Authorization: Bearer $CLOUDDNS_TOKEN" + + if [ "$method" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$CLOUDDNS_API/$endpoint" "" "$method" | tr -d '\t\r\n ')" + else + response="$(_get "$CLOUDDNS_API/$endpoint" | tr -d '\t\r\n ')" + fi + + # shellcheck disable=SC2181 + if [ "$?" != "0" ]; then + _err "Error $endpoint" + return 1 + fi + _debug2 response "$response" + return 0 +} + +# Returns: +# CLOUDDNS_TOKEN=dslfje2rj23l +_clouddns_login() { + login_data="{\"email\": \"$CLOUDDNS_EMAIL\", \"password\": \"$CLOUDDNS_PASSWORD\"}" + response="$(_post "$login_data" "$CLOUDDNS_LOGIN_API" "" "POST" "Content-Type: application/json")" + + if _contains "$response" "\"accessToken\":\""; then + CLOUDDNS_TOKEN=$(echo "$response" | _egrep_o "\"accessToken\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \") + export CLOUDDNS_TOKEN + else + echo 'Could not get CloudDNS access token; check your credentials' + return 1 + fi + return 0 +} diff --git a/dnsapi/dns_cloudns.sh b/dnsapi/dns_cloudns.sh index df824e86..8d7fd437 100755 --- a/dnsapi/dns_cloudns.sh +++ b/dnsapi/dns_cloudns.sh @@ -2,11 +2,14 @@ # Author: Boyan Peychev # Repository: https://github.com/ClouDNS/acme.sh/ +# Editor: I Komang Suryadana #CLOUDNS_AUTH_ID=XXXXX #CLOUDNS_SUB_AUTH_ID=XXXXX #CLOUDNS_AUTH_PASSWORD="YYYYYYYYY" CLOUDNS_API="https://api.cloudns.net" +DOMAIN_TYPE= +DOMAIN_MASTER= ######## Public functions ##################### @@ -61,15 +64,24 @@ dns_cloudns_rm() { host="$(echo "$1" | sed "s/\.$zone\$//")" record=$2 + _dns_cloudns_get_zone_info "$zone" + + _debug "Type" "$DOMAIN_TYPE" + _debug "Cloud Master" "$DOMAIN_MASTER" + if _contains "$DOMAIN_TYPE" "cloud"; then + zone=$DOMAIN_MASTER + fi + _debug "ZONE" "$zone" + _dns_cloudns_http_api_call "dns/records.json" "domain-name=$zone&host=$host&type=TXT" if ! _contains "$response" "\"id\":"; then return 1 fi - for i in $(echo "$response" | tr '{' "\n" | grep "$record"); do + for i in $(echo "$response" | tr '{' "\n" | grep -- "$record"); do record_id=$(echo "$i" | tr ',' "\n" | grep -E '^"id"' | sed -re 's/^\"id\"\:\"([0-9]+)\"$/\1/g') - if [ ! -z "$record_id" ]; then + if [ -n "$record_id" ]; then _debug zone "$zone" _debug host "$host" _debug record "$record" @@ -91,7 +103,7 @@ dns_cloudns_rm() { #################### Private functions below ################################## _dns_cloudns_init_check() { - if [ ! -z "$CLOUDNS_INIT_CHECK_COMPLETED" ]; then + if [ -n "$CLOUDNS_INIT_CHECK_COMPLETED" ]; then return 0 fi @@ -134,6 +146,18 @@ _dns_cloudns_init_check() { return 0 } +_dns_cloudns_get_zone_info() { + zone=$1 + _dns_cloudns_http_api_call "dns/get-zone-info.json" "domain-name=$zone" + if ! _contains "$response" "\"status\":\"Failed\""; then + DOMAIN_TYPE=$(echo "$response" | _egrep_o '"type":"[^"]*"' | cut -d : -f 2 | tr -d '"') + if _contains "$DOMAIN_TYPE" "cloud"; then + DOMAIN_MASTER=$(echo "$response" | _egrep_o '"cloud-master":"[^"]*"' | cut -d : -f 2 | tr -d '"') + fi + fi + return 0 +} + _dns_cloudns_get_zone_name() { i=2 while true; do @@ -164,7 +188,7 @@ _dns_cloudns_http_api_call() { _debug CLOUDNS_SUB_AUTH_ID "$CLOUDNS_SUB_AUTH_ID" _debug CLOUDNS_AUTH_PASSWORD "$CLOUDNS_AUTH_PASSWORD" - if [ ! -z "$CLOUDNS_SUB_AUTH_ID" ]; then + if [ -n "$CLOUDNS_SUB_AUTH_ID" ]; then auth_user="sub-auth-id=$CLOUDNS_SUB_AUTH_ID" else auth_user="auth-id=$CLOUDNS_AUTH_ID" diff --git a/dnsapi/dns_cn.sh b/dnsapi/dns_cn.sh new file mode 100644 index 00000000..38d1f4aa --- /dev/null +++ b/dnsapi/dns_cn.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env sh + +# DNS API for acme.sh for Core-Networks (https://beta.api.core-networks.de/doc/). +# created by 5ll and francis + +CN_API="https://beta.api.core-networks.de" + +######## Public functions ##################### + +dns_cn_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _cn_login; then + _err "login failed" + return 1 + fi + + _debug "First detect the root zone" + if ! _cn_get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug "_sub_domain $_sub_domain" + _debug "_domain $_domain" + + _info "Adding record" + curData="{\"name\":\"$_sub_domain\",\"ttl\":120,\"type\":\"TXT\",\"data\":\"$txtvalue\"}" + curResult="$(_post "${curData}" "${CN_API}/dnszones/${_domain}/records/")" + + _debug "curData $curData" + _debug "curResult $curResult" + + if _contains "$curResult" ""; then + _info "Added, OK" + + if ! _cn_commit; then + _err "commiting changes failed" + return 1 + fi + return 0 + + else + _err "Add txt record error." + _debug "curData is $curData" + _debug "curResult is $curResult" + _err "error adding text record, response was $curResult" + return 1 + fi +} + +dns_cn_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _cn_login; then + _err "login failed" + return 1 + fi + + _debug "First detect the root zone" + if ! _cn_get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _info "Deleting record" + curData="{\"name\":\"$_sub_domain\",\"data\":\"$txtvalue\"}" + curResult="$(_post "${curData}" "${CN_API}/dnszones/${_domain}/records/delete")" + _debug curData is "$curData" + + _info "commiting changes" + if ! _cn_commit; then + _err "commiting changes failed" + return 1 + fi + + _info "Deletet txt record" + return 0 +} + +################### Private functions below ################################## +_cn_login() { + CN_User="${CN_User:-$(_readaccountconf_mutable CN_User)}" + CN_Password="${CN_Password:-$(_readaccountconf_mutable CN_Password)}" + if [ -z "$CN_User" ] || [ -z "$CN_Password" ]; then + CN_User="" + CN_Password="" + _err "You must export variables: CN_User and CN_Password" + return 1 + fi + + #save the config variables to the account conf file. + _saveaccountconf_mutable CN_User "$CN_User" + _saveaccountconf_mutable CN_Password "$CN_Password" + + _info "Getting an AUTH-Token" + curData="{\"login\":\"${CN_User}\",\"password\":\"${CN_Password}\"}" + curResult="$(_post "${curData}" "${CN_API}/auth/token")" + _debug "Calling _CN_login: '${curData}' '${CN_API}/auth/token'" + + if _contains "${curResult}" '"token":"'; then + authToken=$(echo "${curResult}" | cut -d ":" -f2 | cut -d "," -f1 | sed 's/^.\(.*\).$/\1/') + export _H1="Authorization: Bearer $authToken" + _info "Successfully acquired AUTH-Token" + _debug "AUTH-Token: '${authToken}'" + _debug "_H1 '${_H1}'" + else + _err "Couldn't acquire an AUTH-Token" + return 1 + fi +} + +# Commit changes +_cn_commit() { + _info "Commiting changes" + _post "" "${CN_API}/dnszones/$h/records/commit" +} + +_cn_get_root() { + domain=$1 + i=2 + p=1 + while true; do + + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + _debug _H1 "${_H1}" + + if [ -z "$h" ]; then + #not valid + return 1 + fi + + _cn_zonelist="$(_get ${CN_API}/dnszones/)" + _debug _cn_zonelist "${_cn_zonelist}" + + if [ "$?" != "0" ]; then + _err "something went wrong while getting the zone list" + return 1 + fi + + if _contains "$_cn_zonelist" "\"name\":\"$h\"" >/dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + else + _debug "Zonelist does not contain domain - iterating " + fi + p=$i + i=$(_math "$i" + 1) + + done + _err "Zonelist does not contain domain - exiting" + return 1 +} diff --git a/dnsapi/dns_conoha.sh b/dnsapi/dns_conoha.sh index d3bee130..ddc32074 100755 --- a/dnsapi/dns_conoha.sh +++ b/dnsapi/dns_conoha.sh @@ -115,9 +115,9 @@ dns_conoha_rm() { return 1 fi - record_id=$(printf "%s" "$response" | _egrep_o '{[^}]*}' \ - | grep '"type":"TXT"' | grep "\"data\":\"$txtvalue\"" | _egrep_o "\"id\":\"[^\"]*\"" \ - | _head_n 1 | cut -d : -f 2 | tr -d \") + record_id=$(printf "%s" "$response" | _egrep_o '{[^}]*}' | + grep '"type":"TXT"' | grep "\"data\":\"$txtvalue\"" | _egrep_o "\"id\":\"[^\"]*\"" | + _head_n 1 | cut -d : -f 2 | tr -d \") if [ -z "$record_id" ]; then _err "Can not get record id to remove." return 1 diff --git a/dnsapi/dns_constellix.sh b/dnsapi/dns_constellix.sh new file mode 100644 index 00000000..69d216f0 --- /dev/null +++ b/dnsapi/dns_constellix.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env sh + +# Author: Wout Decre + +CONSTELLIX_Api="https://api.dns.constellix.com/v1" +#CONSTELLIX_Key="XXX" +#CONSTELLIX_Secret="XXX" + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +dns_constellix_add() { + fulldomain=$1 + txtvalue=$2 + + CONSTELLIX_Key="${CONSTELLIX_Key:-$(_readaccountconf_mutable CONSTELLIX_Key)}" + CONSTELLIX_Secret="${CONSTELLIX_Secret:-$(_readaccountconf_mutable CONSTELLIX_Secret)}" + + if [ -z "$CONSTELLIX_Key" ] || [ -z "$CONSTELLIX_Secret" ]; then + _err "You did not specify the Contellix API key and secret yet." + return 1 + fi + + _saveaccountconf_mutable CONSTELLIX_Key "$CONSTELLIX_Key" + _saveaccountconf_mutable CONSTELLIX_Secret "$CONSTELLIX_Secret" + + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + # 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 + _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 +# Used to remove the txt record after validation +dns_constellix_rm() { + fulldomain=$1 + txtvalue=$2 + + CONSTELLIX_Key="${CONSTELLIX_Key:-$(_readaccountconf_mutable CONSTELLIX_Key)}" + CONSTELLIX_Secret="${CONSTELLIX_Secret:-$(_readaccountconf_mutable CONSTELLIX_Secret)}" + + if [ -z "$CONSTELLIX_Key" ] || [ -z "$CONSTELLIX_Secret" ]; then + _err "You did not specify the Contellix API key and secret yet." + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + # 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 + _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 ################################## + +_get_root() { + domain=$1 + i=2 + p=1 + _debug "Detecting root zone" + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + return 1 + fi + + if ! _constellix_rest GET "domains/search?exact=$h"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\""; then + _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" + + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_constellix_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + rdate=$(date +"%s")"000" + hmac=$(printf "%s" "$rdate" | _hmac sha1 "$(printf "%s" "$CONSTELLIX_Secret" | _hex_dump | tr -d ' ')" | _base64) + + export _H1="x-cnsdns-apiKey: $CONSTELLIX_Key" + export _H2="x-cnsdns-requestDate: $rdate" + export _H3="x-cnsdns-hmac: $hmac" + export _H4="Accept: application/json" + export _H5="Content-Type: application/json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$CONSTELLIX_Api/$ep" "" "$m")" + else + response="$(_get "$CONSTELLIX_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "Error $ep" + return 1 + fi + + _debug response "$response" + return 0 +} diff --git a/dnsapi/dns_cpanel.sh b/dnsapi/dns_cpanel.sh new file mode 100755 index 00000000..f6126bcb --- /dev/null +++ b/dnsapi/dns_cpanel.sh @@ -0,0 +1,160 @@ +#!/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" | _egrep_o '"[a-z0-9\.\-]*":\["; cPanel first' | cut -d':' -f1 | 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" | _egrep_o 'data":\[[^]]*]' | grep -q '"newserial":null'); then return 1; fi + return 0 +} + +_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 -e "s/},{/},\n{/g" | grep "$fulldomain" | grep "$txtvalue" | _egrep_o 'line":[0-9]+' | cut -d ':' -f 2) + _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_curanet.sh b/dnsapi/dns_curanet.sh new file mode 100644 index 00000000..4b39f365 --- /dev/null +++ b/dnsapi/dns_curanet.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env sh + +#Script to use with curanet.dk, scannet.dk, wannafind.dk, dandomain.dk DNS management. +#Requires api credentials with scope: dns +#Author: Peter L. Hansen +#Version 1.0 + +CURANET_REST_URL="https://api.curanet.dk/dns/v1/Domains" +CURANET_AUTH_URL="https://apiauth.dk.team.blue/auth/realms/Curanet/protocol/openid-connect/token" +CURANET_ACCESS_TOKEN="" + +######## Public functions ##################### + +#Usage: dns_curanet_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_curanet_add() { + fulldomain=$1 + txtvalue=$2 + _info "Using curanet" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + CURANET_AUTHCLIENTID="${CURANET_AUTHCLIENTID:-$(_readaccountconf_mutable CURANET_AUTHCLIENTID)}" + CURANET_AUTHSECRET="${CURANET_AUTHSECRET:-$(_readaccountconf_mutable CURANET_AUTHSECRET)}" + if [ -z "$CURANET_AUTHCLIENTID" ] || [ -z "$CURANET_AUTHSECRET" ]; then + CURANET_AUTHCLIENTID="" + CURANET_AUTHSECRET="" + _err "You don't specify curanet api client and secret." + _err "Please create your auth info and try again." + return 1 + fi + + #save the credentials to the account conf file. + _saveaccountconf_mutable CURANET_AUTHCLIENTID "$CURANET_AUTHCLIENTID" + _saveaccountconf_mutable CURANET_AUTHSECRET "$CURANET_AUTHSECRET" + + if ! _get_token; then + _err "Unable to get token" + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + export _H1="Content-Type: application/json-patch+json" + export _H2="Accept: application/json" + export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN" + data="{\"name\": \"$fulldomain\",\"type\": \"TXT\",\"ttl\": 60,\"priority\": 0,\"data\": \"$txtvalue\"}" + response="$(_post "$data" "$CURANET_REST_URL/${_domain}/Records" "" "")" + + if _contains "$response" "$txtvalue"; then + _debug "TXT record added OK" + else + _err "Unable to add TXT record" + return 1 + fi + + return 0 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_curanet_rm() { + fulldomain=$1 + txtvalue=$2 + _info "Using curanet" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + CURANET_AUTHCLIENTID="${CURANET_AUTHCLIENTID:-$(_readaccountconf_mutable CURANET_AUTHCLIENTID)}" + CURANET_AUTHSECRET="${CURANET_AUTHSECRET:-$(_readaccountconf_mutable CURANET_AUTHSECRET)}" + + if ! _get_token; then + _err "Unable to get token" + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + _debug "Getting current record list to identify TXT to delete" + + export _H1="Content-Type: application/json" + export _H2="Accept: application/json" + export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN" + + response="$(_get "$CURANET_REST_URL/${_domain}/Records" "" "")" + + if ! _contains "$response" "$txtvalue"; then + _err "Unable to delete record (does not contain $txtvalue )" + return 1 + fi + + recordid=$(echo "$response" | _egrep_o "{\"id\":[0-9]+,\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":60,\"priority\":0,\"data\":\"..$txtvalue" | _egrep_o "id\":[0-9]+" | cut -c 5-) + + if [ -z "$recordid" ]; then + _err "Unable to get recordid" + _debug "regex {\"id\":[0-9]+,\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":60,\"priority\":0,\"data\":\"..$txtvalue" + _debug "response $response" + return 1 + fi + + _debug "Deleting recordID $recordid" + response="$(_post "" "$CURANET_REST_URL/${_domain}/Records/$recordid" "" "DELETE")" + return 0 +} + +#################### Private functions below ################################## + +_get_token() { + response="$(_post "grant_type=client_credentials&client_id=$CURANET_AUTHCLIENTID&client_secret=$CURANET_AUTHSECRET&scope=dns" "$CURANET_AUTH_URL" "" "")" + if ! _contains "$response" "access_token"; then + _err "Unable get access token" + return 1 + fi + CURANET_ACCESS_TOKEN=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]+" | cut -c 17-) + + if [ -z "$CURANET_ACCESS_TOKEN" ]; then + _err "Unable to get token" + return 1 + fi + + return 0 + +} + +#_acme-challenge.www.domain.com +#returns +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_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 + #not valid + return 1 + fi + + export _H1="Content-Type: application/json" + export _H2="Accept: application/json" + export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN" + response="$(_get "$CURANET_REST_URL/$h/Records" "" "")" + + if [ ! "$(echo "$response" | _egrep_o "Entity not found")" ]; then + _domain=$h + return 0 + fi + + i=$(_math "$i" + 1) + done + return 1 +} diff --git a/dnsapi/dns_cx.sh b/dnsapi/dns_cx.sh deleted file mode 100755 index d07d8e0c..00000000 --- a/dnsapi/dns_cx.sh +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env sh - -# CloudXNS Domain api -# -#CX_Key="1234" -# -#CX_Secret="sADDsdasdgdsf" - -CX_Api="https://www.cloudxns.net/api2" - -#REST_API -######## Public functions ##################### - -#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_cx_add() { - fulldomain=$1 - txtvalue=$2 - - if [ -z "$CX_Key" ] || [ -z "$CX_Secret" ]; then - CX_Key="" - CX_Secret="" - _err "You don't specify cloudxns.net api key or secret yet." - _err "Please create you key and try again." - return 1 - fi - - REST_API="$CX_Api" - - #save the api key and email to the account conf file. - _saveaccountconf CX_Key "$CX_Key" - _saveaccountconf CX_Secret "$CX_Secret" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" - return 1 - fi - - add_record "$_domain" "$_sub_domain" "$txtvalue" -} - -#fulldomain txtvalue -dns_cx_rm() { - fulldomain=$1 - txtvalue=$2 - REST_API="$CX_Api" - if _get_root "$fulldomain"; then - record_id="" - existing_records "$_domain" "$_sub_domain" "$txtvalue" - if [ "$record_id" ]; then - _rest DELETE "record/$record_id/$_domain_id" "{}" - _info "Deleted record ${fulldomain}" - fi - fi -} - -#usage: root sub -#return if the sub record already exists. -#echos the existing records count. -# '0' means doesn't exist -existing_records() { - _debug "Getting txt records" - root=$1 - sub=$2 - if ! _rest GET "record/$_domain_id?:domain_id?host_id=0&offset=0&row_num=100"; then - return 1 - fi - - seg=$(printf "%s\n" "$response" | _egrep_o '"record_id":[^{]*host":"'"$_sub_domain"'"[^}]*\}') - _debug seg "$seg" - if [ -z "$seg" ]; then - return 0 - fi - - if printf "%s" "$response" | grep '"type":"TXT"' >/dev/null; then - record_id=$(printf "%s\n" "$seg" | _egrep_o '"record_id":"[^"]*"' | cut -d : -f 2 | tr -d \" | _head_n 1) - _debug record_id "$record_id" - return 0 - fi - -} - -#add the txt record. -#usage: root sub txtvalue -add_record() { - root=$1 - sub=$2 - txtvalue=$3 - fulldomain="$sub.$root" - - _info "Adding record" - - if ! _rest POST "record" "{\"domain_id\": $_domain_id, \"host\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"type\":\"TXT\",\"ttl\":600, \"line_id\":1}"; then - return 1 - 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=2 - p=1 - - if ! _rest GET "domain"; 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" "$h."; then - seg=$(printf "%s\n" "$response" | _egrep_o '"id":[^{]*"'"$h"'."[^}]*}') - _debug seg "$seg" - _domain_id=$(printf "%s\n" "$seg" | _egrep_o "\"id\":\"[^\"]*\"" | 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) - _debug _sub_domain "$_sub_domain" - _domain="$h" - _debug _domain "$_domain" - return 0 - fi - return 1 - fi - p="$i" - i=$(_math "$i" + 1) - done - return 1 -} - -#Usage: method URI data -_rest() { - m=$1 - ep="$2" - _debug ep "$ep" - url="$REST_API/$ep" - _debug url "$url" - - cdate=$(date -u "+%Y-%m-%d %H:%M:%S UTC") - _debug cdate "$cdate" - - data="$3" - _debug data "$data" - - sec="$CX_Key$url$data$cdate$CX_Secret" - _debug sec "$sec" - hmac=$(printf "%s" "$sec" | _digest md5 hex) - _debug hmac "$hmac" - - export _H1="API-KEY: $CX_Key" - export _H2="API-REQUEST-DATE: $cdate" - export _H3="API-HMAC: $hmac" - export _H4="Content-Type: application/json" - - if [ "$data" ]; then - response="$(_post "$data" "$url" "" "$m")" - else - response="$(_get "$url")" - fi - - if [ "$?" != "0" ]; then - _err "error $ep" - return 1 - fi - _debug2 response "$response" - - _contains "$response" '"code":1' - -} diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh index d7ad712c..830e8831 100644 --- a/dnsapi/dns_cyon.sh +++ b/dnsapi/dns_cyon.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh ######## -# Custom cyon.ch DNS API for use with [acme.sh](https://github.com/Neilpang/acme.sh) +# Custom cyon.ch DNS API for use with [acme.sh](https://github.com/acmesh-official/acme.sh) # # Usage: acme.sh --issue --dns dns_cyon -d www.domain.com # @@ -18,23 +18,23 @@ ######## dns_cyon_add() { - _cyon_load_credentials \ - && _cyon_load_parameters "$@" \ - && _cyon_print_header "add" \ - && _cyon_login \ - && _cyon_change_domain_env \ - && _cyon_add_txt \ - && _cyon_logout + _cyon_load_credentials && + _cyon_load_parameters "$@" && + _cyon_print_header "add" && + _cyon_login && + _cyon_change_domain_env && + _cyon_add_txt && + _cyon_logout } dns_cyon_rm() { - _cyon_load_credentials \ - && _cyon_load_parameters "$@" \ - && _cyon_print_header "delete" \ - && _cyon_login \ - && _cyon_change_domain_env \ - && _cyon_delete_txt \ - && _cyon_logout + _cyon_load_credentials && + _cyon_load_parameters "$@" && + _cyon_print_header "delete" && + _cyon_login && + _cyon_change_domain_env && + _cyon_delete_txt && + _cyon_logout } ######################### @@ -44,7 +44,7 @@ dns_cyon_rm() { _cyon_load_credentials() { # Convert loaded password to/from base64 as needed. if [ "${CY_Password_B64}" ]; then - CY_Password="$(printf "%s" "${CY_Password_B64}" | _dbase64 "multiline")" + CY_Password="$(printf "%s" "${CY_Password_B64}" | _dbase64)" elif [ "${CY_Password}" ]; then CY_Password_B64="$(printf "%s" "${CY_Password}" | _base64)" fi @@ -66,7 +66,7 @@ _cyon_load_credentials() { _debug "Save credentials to account.conf" _saveaccountconf CY_Username "${CY_Username}" _saveaccountconf CY_Password_B64 "$CY_Password_B64" - if [ ! -z "${CY_OTP_Secret}" ]; then + if [ -n "${CY_OTP_Secret}" ]; then _saveaccountconf CY_OTP_Secret "$CY_OTP_Secret" else _clearaccountconf CY_OTP_Secret @@ -164,7 +164,7 @@ _cyon_login() { # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request. # 2FA authentication with OTP? - if [ ! -z "${CY_OTP_Secret}" ]; then + if [ -n "${CY_OTP_Secret}" ]; then _info " - Authorising with OTP code..." if ! _exists oathtool; then diff --git a/dnsapi/dns_da.sh b/dnsapi/dns_da.sh index 7755c7e1..4d3e09b1 100755 --- a/dnsapi/dns_da.sh +++ b/dnsapi/dns_da.sh @@ -9,7 +9,7 @@ # # User must provide login data and URL to DirectAdmin incl. port. # You can create login key, by using the Login Keys function -# ( https://da.example.com:8443/CMD_LOGIN_KEYS ), which only has access to +# ( https://da.example.com:8443/CMD_LOGIN_KEYS ), which only has access to # - CMD_API_DNS_CONTROL # - CMD_API_SHOW_DOMAINS # @@ -115,23 +115,23 @@ _da_api() { _debug response "$response" case "${cmd}" in - CMD_API_DNS_CONTROL) - # Parse the result in general - # error=0&text=Records Deleted&details= - # error=1&text=Cannot View Dns Record&details=No domain provided - err_field="$(_getfield "$response" 1 '&')" - txt_field="$(_getfield "$response" 2 '&')" - details_field="$(_getfield "$response" 3 '&')" - error="$(_getfield "$err_field" 2 '=')" - text="$(_getfield "$txt_field" 2 '=')" - details="$(_getfield "$details_field" 2 '=')" - _debug "error: ${error}, text: ${text}, details: ${details}" - if [ "$error" != "0" ]; then - _err "error $response" - return 1 - fi - ;; - CMD_API_SHOW_DOMAINS) ;; + CMD_API_DNS_CONTROL) + # Parse the result in general + # error=0&text=Records Deleted&details= + # error=1&text=Cannot View Dns Record&details=No domain provided + err_field="$(_getfield "$response" 1 '&')" + txt_field="$(_getfield "$response" 2 '&')" + details_field="$(_getfield "$response" 3 '&')" + error="$(_getfield "$err_field" 2 '=')" + text="$(_getfield "$txt_field" 2 '=')" + details="$(_getfield "$details_field" 2 '=')" + _debug "error: ${error}, text: ${text}, details: ${details}" + if [ "$error" != "0" ]; then + _err "error $response" + return 1 + fi + ;; + CMD_API_SHOW_DOMAINS) ;; esac return 0 } diff --git a/dnsapi/dns_ddnss.sh b/dnsapi/dns_ddnss.sh new file mode 100644 index 00000000..b9da33ff --- /dev/null +++ b/dnsapi/dns_ddnss.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env sh + +#Created by RaidenII, to use DuckDNS's API to add/remove text records +#modified by helbgd @ 03/13/2018 to support ddnss.de +#modified by mod242 @ 04/24/2018 to support different ddnss domains +#Please note: the Wildcard Feature must be turned on for the Host record +#and the checkbox for TXT needs to be enabled + +# Pass credentials before "acme.sh --issue --dns dns_ddnss ..." +# -- +# export DDNSS_Token="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" +# -- +# + +DDNSS_DNS_API="https://ddnss.de/upd.php" + +######## Public functions ##################### + +#Usage: dns_ddnss_add _acme-challenge.domain.ddnss.de "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_ddnss_add() { + fulldomain=$1 + txtvalue=$2 + + DDNSS_Token="${DDNSS_Token:-$(_readaccountconf_mutable DDNSS_Token)}" + if [ -z "$DDNSS_Token" ]; then + _err "You must export variable: DDNSS_Token" + _err "The token for your DDNSS account is necessary." + _err "You can look it up in your DDNSS account." + return 1 + fi + + # Now save the credentials. + _saveaccountconf_mutable DDNSS_Token "$DDNSS_Token" + + # Unfortunately, DDNSS does not seems to support lookup domain through API + # So I assume your credentials (which are your domain and token) are correct + # If something goes wrong, we will get a KO response from DDNSS + + if ! _ddnss_get_domain; then + return 1 + fi + + # Now add the TXT record to DDNSS DNS + _info "Trying to add TXT record" + if _ddnss_rest GET "key=$DDNSS_Token&host=$_ddnss_domain&txtm=1&txt=$txtvalue"; then + if [ "$response" = "Updated 1 hostname." ]; then + _info "TXT record has been successfully added to your DDNSS domain." + _info "Note that all subdomains under this domain uses the same TXT record." + return 0 + else + _err "Errors happened during adding the TXT record, response=$response" + return 1 + fi + else + _err "Errors happened during adding the TXT record." + return 1 + fi +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_ddnss_rm() { + fulldomain=$1 + txtvalue=$2 + + DDNSS_Token="${DDNSS_Token:-$(_readaccountconf_mutable DDNSS_Token)}" + if [ -z "$DDNSS_Token" ]; then + _err "You must export variable: DDNSS_Token" + _err "The token for your DDNSS account is necessary." + _err "You can look it up in your DDNSS account." + return 1 + fi + + if ! _ddnss_get_domain; then + return 1 + fi + + # Now remove the TXT record from DDNS DNS + _info "Trying to remove TXT record" + if _ddnss_rest GET "key=$DDNSS_Token&host=$_ddnss_domain&txtm=2"; then + if [ "$response" = "Updated 1 hostname." ]; then + _info "TXT record has been successfully removed from your DDNSS domain." + return 0 + else + _err "Errors happened during removing the TXT record, response=$response" + return 1 + fi + else + _err "Errors happened during removing the TXT record." + return 1 + fi +} + +#################### Private functions below ################################## + +#fulldomain=_acme-challenge.domain.ddnss.de +#returns +# _ddnss_domain=domain +_ddnss_get_domain() { + + # We'll extract the domain/username from full domain + _ddnss_domain="$(echo "$fulldomain" | _lower_case | _egrep_o '[.][^.][^.]*[.](ddnss|dyn-ip24|dyndns|dyn|dyndns1|home-webserver|myhome-server|dynip)\..*' | cut -d . -f 2-)" + + if [ -z "$_ddnss_domain" ]; then + _err "Error extracting the domain." + return 1 + fi + + return 0 +} + +#Usage: method URI +_ddnss_rest() { + method=$1 + param="$2" + _debug param "$param" + url="$DDNSS_DNS_API?$param" + _debug url "$url" + + # DDNSS uses GET to update domain info + if [ "$method" = "GET" ]; then + response="$(_get "$url" | sed 's/<[a-zA-Z\/][^>]*>//g' | tr -s "\n" | _tail_n 1)" + else + _err "Unsupported method" + return 1 + fi + + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_desec.sh b/dnsapi/dns_desec.sh new file mode 100644 index 00000000..495a6780 --- /dev/null +++ b/dnsapi/dns_desec.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env sh +# +# deSEC.io Domain API +# +# Author: Zheng Qian +# +# deSEC API doc +# https://desec.readthedocs.io/en/latest/ + +REST_API="https://desec.io/api/v1/domains" + +######## Public functions ##################### + +#Usage: dns_desec_add _acme-challenge.foobar.dedyn.io "d41d8cd98f00b204e9800998ecf8427e" +dns_desec_add() { + fulldomain=$1 + txtvalue=$2 + _info "Using desec.io api" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + DEDYN_TOKEN="${DEDYN_TOKEN:-$(_readaccountconf_mutable DEDYN_TOKEN)}" + + if [ -z "$DEDYN_TOKEN" ]; then + DEDYN_TOKEN="" + _err "You did not specify DEDYN_TOKEN yet." + _err "Please create your key and try again." + _err "e.g." + _err "export DEDYN_TOKEN=d41d8cd98f00b204e9800998ecf8427e" + return 1 + fi + #save the api token to the account conf file. + _saveaccountconf_mutable DEDYN_TOKEN "$DEDYN_TOKEN" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain" "$REST_API/"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + # Get existing TXT record + _debug "Getting txt records" + txtvalues="\"\\\"$txtvalue\\\"\"" + _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")" + _debug "existing TXT found" + _debug oldtxtvalues "$oldtxtvalues" + if [ -n "$oldtxtvalues" ]; then + for oldtxtvalue in $oldtxtvalues; do + txtvalues="$txtvalues, \"\\\"$oldtxtvalue\\\"\"" + done + fi + fi + _debug txtvalues "$txtvalues" + _info "Adding record" + body="[{\"subname\":\"$_sub_domain\", \"type\":\"TXT\", \"records\":[$txtvalues], \"ttl\":3600}]" + + if _desec_rest PUT "$REST_API/$_domain/rrsets/" "$body"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + + _err "Add txt record error." + return 1 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_desec_rm() { + fulldomain=$1 + txtvalue=$2 + _info "Using desec.io api" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + DEDYN_TOKEN="${DEDYN_TOKEN:-$(_readaccountconf_mutable DEDYN_TOKEN)}" + + if [ -z "$DEDYN_TOKEN" ]; then + DEDYN_TOKEN="" + _err "You did not specify DEDYN_TOKEN yet." + _err "Please create your key and try again." + _err "e.g." + _err "export DEDYN_TOKEN=d41d8cd98f00b204e9800998ecf8427e" + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain" "$REST_API/"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + # Get existing TXT record + _debug "Getting txt records" + txtvalues="" + _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")" + _debug "existing TXT found" + _debug oldtxtvalues "$oldtxtvalues" + if [ -n "$oldtxtvalues" ]; then + for oldtxtvalue in $oldtxtvalues; do + if [ "$txtvalue" != "$oldtxtvalue" ]; then + txtvalues="$txtvalues, \"\\\"$oldtxtvalue\\\"\"" + fi + done + fi + fi + txtvalues="$(echo "$txtvalues" | cut -c3-)" + _debug txtvalues "$txtvalues" + + _info "Deleting record" + body="[{\"subname\":\"$_sub_domain\", \"type\":\"TXT\", \"records\":[$txtvalues], \"ttl\":3600}]" + _desec_rest PUT "$REST_API/$_domain/rrsets/" "$body" + if [ "$_code" = "200" ]; then + _info "Deleted, OK" + return 0 + fi + + _err "Delete txt record error." + return 1 +} + +#################### Private functions below ################################## + +_desec_rest() { + m="$1" + ep="$2" + data="$3" + + export _H1="Authorization: Token $DEDYN_TOKEN" + export _H2="Accept: application/json" + export _H3="Content-Type: application/json" + + if [ "$m" != "GET" ]; then + _secure_debug2 data "$data" + response="$(_post "$data" "$ep" "" "$m")" + else + response="$(_get "$ep")" + fi + _ret="$?" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug "http response code $_code" + _secure_debug2 response "$response" + if [ "$_ret" != "0" ]; then + _err "error $ep" + return 1 + fi + + response="$(printf "%s" "$response" | _normalizeJson)" + return 0 +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain="$1" + ep="$2" + i=2 + 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 ! _desec_rest GET "$ep"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} diff --git a/dnsapi/dns_df.sh b/dnsapi/dns_df.sh new file mode 100644 index 00000000..c0499ddf --- /dev/null +++ b/dnsapi/dns_df.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env sh + +######################################################################## +# https://dyndnsfree.de hook script for acme.sh +# +# Environment variables: +# +# - $DF_user (your dyndnsfree.de username) +# - $DF_password (your dyndnsfree.de password) +# +# Author: Thilo Gass +# Git repo: https://github.com/ThiloGa/acme.sh + +#-- dns_df_add() - Add TXT record -------------------------------------- +# Usage: dns_df_add _acme-challenge.subdomain.domain.com "XyZ123..." + +dyndnsfree_api="https://dynup.de/acme.php" + +dns_df_add() { + fulldomain=$1 + txt_value=$2 + _info "Using DNS-01 dyndnsfree.de hook" + + DF_user="${DF_user:-$(_readaccountconf_mutable DF_user)}" + DF_password="${DF_password:-$(_readaccountconf_mutable DF_password)}" + if [ -z "$DF_user" ] || [ -z "$DF_password" ]; then + DF_user="" + DF_password="" + _err "No auth details provided. Please set user credentials using the \$DF_user and \$DF_password environment variables." + return 1 + fi + #save the api user and password to the account conf file. + _debug "Save user and password" + _saveaccountconf_mutable DF_user "$DF_user" + _saveaccountconf_mutable DF_password "$DF_password" + + domain="$(printf "%s" "$fulldomain" | cut -d"." -f2-)" + + get="$dyndnsfree_api?username=$DF_user&password=$DF_password&hostname=$domain&add_hostname=$fulldomain&txt=$txt_value" + + if ! erg="$(_get "$get")"; then + _err "error Adding $fulldomain TXT: $txt_value" + return 1 + fi + + if _contains "$erg" "success"; then + _info "Success, TXT Added, OK" + else + _err "error Adding $fulldomain TXT: $txt_value erg: $erg" + return 1 + fi + + _debug "ok Auto $fulldomain TXT: $txt_value erg: $erg" + return 0 +} + +dns_df_rm() { + + fulldomain=$1 + txtvalue=$2 + _info "TXT enrty in $fulldomain is deleted automatically" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + +} diff --git a/dnsapi/dns_dgon.sh b/dnsapi/dns_dgon.sh index 5d38ef76..afe1b32e 100755 --- a/dnsapi/dns_dgon.sh +++ b/dnsapi/dns_dgon.sh @@ -22,7 +22,7 @@ dns_dgon_add() { txtvalue=$2 DO_API_KEY="${DO_API_KEY:-$(_readaccountconf_mutable DO_API_KEY)}" - # Check if API Key Exist + # Check if API Key Exists if [ -z "$DO_API_KEY" ]; then DO_API_KEY="" _err "You did not specify DigitalOcean API key." @@ -77,7 +77,7 @@ dns_dgon_rm() { txtvalue=$2 DO_API_KEY="${DO_API_KEY:-$(_readaccountconf_mutable DO_API_KEY)}" - # Check if API Key Exist + # Check if API Key Exists if [ -z "$DO_API_KEY" ]; then DO_API_KEY="" _err "You did not specify DigitalOcean API key." @@ -104,48 +104,60 @@ dns_dgon_rm() { ## may get: "links":{"pages":{"last":".../v2/domains/DOM/records?page=2","next":".../v2/domains/DOM/records?page=2"}} GURL="https://api.digitalocean.com/v2/domains/$_domain/records" - ## while we dont have a record ID we keep going - while [ -z "$record" ]; do + ## Get all the matching records + while true; do ## 1) get the URL ## the create request - get ## args: URL, [onlyheader, timeout] domain_list="$(_get "$GURL")" - ## 2) find record - ## check for what we are looing for: "type":"A","name":"$_sub_domain" - record="$(echo "$domain_list" | _egrep_o "\"id\"\s*\:\s*\"*[0-9]+\"*[^}]*\"name\"\s*\:\s*\"$_sub_domain\"[^}]*\"data\"\s*\:\s*\"$txtvalue\"")" - ## 3) check record and get next page - if [ -z "$record" ]; then - ## find the next page if we dont have a match - nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=[0-9]+")" - if [ -z "$nextpage" ]; then - _err "no record and no nextpage in digital ocean DNS removal" - return 1 - fi - _debug2 nextpage "$nextpage" - GURL="$nextpage" + + ## check response + if [ "$?" != "0" ]; then + _err "error in domain_list response: $domain_list" + return 1 fi - ## we break out of the loop when we have a record + _debug2 domain_list "$domain_list" + + ## 2) find records + ## check for what we are looking for: "type":"A","name":"$_sub_domain" + record="$(echo "$domain_list" | _egrep_o "\"id\"\s*\:\s*\"*[0-9]+\"*[^}]*\"name\"\s*\:\s*\"$_sub_domain\"[^}]*\"data\"\s*\:\s*\"$txtvalue\"")" + + if [ -n "$record" ]; then + + ## we found records + rec_ids="$(echo "$record" | _egrep_o "id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")" + _debug rec_ids "$rec_ids" + if [ -n "$rec_ids" ]; then + echo "$rec_ids" | while IFS= read -r rec_id; do + ## delete the record + ## delete URL for removing the one we dont want + DURL="https://api.digitalocean.com/v2/domains/$_domain/records/$rec_id" + + ## the create request - delete + ## args: BODY, URL, [need64, httpmethod] + response="$(_post "" "$DURL" "" "DELETE")" + + ## check response (sort of) + if [ "$?" != "0" ]; then + _err "error in remove response: $response" + return 1 + fi + _debug2 response "$response" + + done + fi + fi + + ## 3) find the next page + nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=[0-9]+")" + if [ -z "$nextpage" ]; then + break + fi + _debug2 nextpage "$nextpage" + GURL="$nextpage" + done - ## we found the record - rec_id="$(echo "$record" | _egrep_o "id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")" - _debug rec_id "$rec_id" - - ## delete the record - ## delete URL for removing the one we dont want - DURL="https://api.digitalocean.com/v2/domains/$_domain/records/$rec_id" - - ## the create request - delete - ## args: BODY, URL, [need64, httpmethod] - response="$(_post "" "$DURL" "" "DELETE")" - - ## check response (sort of) - if [ "$?" != "0" ]; then - _err "error in remove response: $response" - return 1 - fi - _debug2 response "$response" - ## finished correctly return 0 } @@ -166,7 +178,7 @@ dns_dgon_rm() { ## _domain="domain.com" _get_base_domain() { # args - fulldomain="$(echo "$1" | tr '[:upper:]' '[:lower:]')" + fulldomain="$(echo "$1" | _lower_case)" _debug fulldomain "$fulldomain" # domain max legal length = 253 @@ -178,44 +190,56 @@ _get_base_domain() { export _H2="Authorization: Bearer $DO_API_KEY" _debug DO_API_KEY "$DO_API_KEY" ## get URL for the list of domains - ## havent seen this request paginated, tested with 18 domains (more requires manual requests with DO) + ## may get: "links":{"pages":{"last":".../v2/domains/DOM/records?page=2","next":".../v2/domains/DOM/records?page=2"}} DOMURL="https://api.digitalocean.com/v2/domains" + found="" - ## get the domain list (DO gives basically a full XFER!) - domain_list="$(_get "$DOMURL")" + ## while we dont have a matching domain we keep going + while [ -z "$found" ]; do + ## get the domain list (current page) + domain_list="$(_get "$DOMURL")" - ## check response - if [ "$?" != "0" ]; then - _err "error in domain_list response: $domain_list" - return 1 - fi - _debug2 domain_list "$domain_list" - - ## for each shortening of our $fulldomain, check if it exists in the $domain_list - ## can never start on 1 (aka whole $fulldomain) as $fulldomain starts with "_acme-challenge" - i=2 - while [ $i -gt 0 ]; do - ## get next longest domain - _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM") - ## check we got something back from our cut (or are we at the end) - if [ -z "$_domain" ]; then - ## we got to the end of the domain - invalid domain - _err "domain not found in DigitalOcean account" + ## check response + if [ "$?" != "0" ]; then + _err "error in domain_list response: $domain_list" return 1 fi - ## we got part of a domain back - grep it out - found="$(echo "$domain_list" | _egrep_o "\"name\"\s*\:\s*\"$_domain\"")" - ## check if it exists - if [ ! -z "$found" ]; then - ## exists - exit loop returning the parts - sub_point=$(_math $i - 1) - _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point") - _debug _domain "$_domain" - _debug _sub_domain "$_sub_domain" - return 0 + _debug2 domain_list "$domain_list" + + i=1 + while [ $i -gt 0 ]; do + ## get next longest domain + _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM") + ## check we got something back from our cut (or are we at the end) + if [ -z "$_domain" ]; then + break + fi + ## we got part of a domain back - grep it out + found="$(echo "$domain_list" | _egrep_o "\"name\"\s*\:\s*\"$_domain\"")" + ## check if it exists + if [ -n "$found" ]; then + ## exists - exit loop returning the parts + sub_point=$(_math $i - 1) + _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point") + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + return 0 + fi + ## increment cut point $i + i=$(_math $i + 1) + done + + if [ -z "$found" ]; then + ## find the next page if we dont have a match + nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=[0-9]+")" + if [ -z "$nextpage" ]; then + _err "no record and no nextpage in digital ocean DNS removal" + return 1 + fi + _debug2 nextpage "$nextpage" + DOMURL="$nextpage" fi - ## increment cut point $i - i=$(_math $i + 1) + done ## we went through the entire domain zone list and dint find one that matched diff --git a/dnsapi/dns_dnsexit.sh b/dnsapi/dns_dnsexit.sh new file mode 100644 index 00000000..62d7d757 --- /dev/null +++ b/dnsapi/dns_dnsexit.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +#use dns-01 at DNSExit.com + +#Author: Samuel Jimenez +#Report Bugs here: https://github.com/acmesh-official/acme.sh + +#DNSEXIT_API_KEY=ABCDEFGHIJ0123456789abcdefghij +#DNSEXIT_AUTH_USER=login@email.address +#DNSEXIT_AUTH_PASS=aStrongPassword +DNSEXIT_API_URL="https://api.dnsexit.com/dns/" +DNSEXIT_HOSTS_URL="https://update.dnsexit.com/ipupdate/hosts.jsp" + +######## Public functions ##################### +#Usage: dns_dnsexit_add _acme-challenge.*.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_dnsexit_add() { + fulldomain=$1 + txtvalue=$2 + _info "Using DNSExit.com" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + _debug 'Load account auth' + if ! get_account_info; then + return 1 + fi + + _debug 'First detect the root zone' + if ! _get_root "$fulldomain"; then + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"add\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":0,\"overwrite\":false}}"; then + _err "$response" + return 1 + fi + + _debug2 _response "$response" + return 0 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_dnsexit_rm() { + fulldomain=$1 + txtvalue=$2 + _info "Using DNSExit.com" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + _debug 'Load account auth' + if ! get_account_info; then + return 1 + fi + + _debug 'First detect the root zone' + if ! _get_root "$fulldomain"; then + _err "$response" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"delete\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"}}"; then + _err "$response" + return 1 + fi + + _debug2 _response "$response" + return 0 +} + +#################### 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 + _domain=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$_domain" + if [ -z "$_domain" ]; then + return 1 + fi + + _debug login "$DNSEXIT_AUTH_USER" + _debug password "$DNSEXIT_AUTH_PASS" + _debug domain "$_domain" + + _dnsexit_http "login=$DNSEXIT_AUTH_USER&password=$DNSEXIT_AUTH_PASS&domain=$_domain" + + if _contains "$response" "0=$_domain"; then + _sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")" + return 0 + else + _debug "Go to next level of $_domain" + fi + i=$(_math "$i" + 1) + done + + return 1 +} + +_dnsexit_rest() { + m=POST + ep="" + data="$1" + _debug _dnsexit_rest "$ep" + _debug data "$data" + + api_key_trimmed=$(echo "$DNSEXIT_API_KEY" | tr -d '"') + + export _H1="apikey: $api_key_trimmed" + export _H2='Content-Type: application/json' + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$DNSEXIT_API_URL/$ep" "" "$m")" + else + response="$(_get "$DNSEXIT_API_URL/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "Error $ep" + return 1 + fi + + _debug2 response "$response" + return 0 +} + +_dnsexit_http() { + m=GET + param="$1" + _debug param "$param" + _debug get "$DNSEXIT_HOSTS_URL?$param" + + response="$(_get "$DNSEXIT_HOSTS_URL?$param")" + + _debug response "$response" + + if [ "$?" != "0" ]; then + _err "Error $param" + return 1 + fi + + _debug2 response "$response" + return 0 +} + +get_account_info() { + + DNSEXIT_API_KEY="${DNSEXIT_API_KEY:-$(_readaccountconf_mutable DNSEXIT_API_KEY)}" + if test -z "$DNSEXIT_API_KEY"; then + DNSEXIT_API_KEY='' + _err 'DNSEXIT_API_KEY was not exported' + return 1 + fi + + _saveaccountconf_mutable DNSEXIT_API_KEY "$DNSEXIT_API_KEY" + + DNSEXIT_AUTH_USER="${DNSEXIT_AUTH_USER:-$(_readaccountconf_mutable DNSEXIT_AUTH_USER)}" + if test -z "$DNSEXIT_AUTH_USER"; then + DNSEXIT_AUTH_USER="" + _err 'DNSEXIT_AUTH_USER was not exported' + return 1 + fi + + _saveaccountconf_mutable DNSEXIT_AUTH_USER "$DNSEXIT_AUTH_USER" + + DNSEXIT_AUTH_PASS="${DNSEXIT_AUTH_PASS:-$(_readaccountconf_mutable DNSEXIT_AUTH_PASS)}" + if test -z "$DNSEXIT_AUTH_PASS"; then + DNSEXIT_AUTH_PASS="" + _err 'DNSEXIT_AUTH_PASS was not exported' + return 1 + fi + + _saveaccountconf_mutable DNSEXIT_AUTH_PASS "$DNSEXIT_AUTH_PASS" + + return 0 +} diff --git a/dnsapi/dns_dnshome.sh b/dnsapi/dns_dnshome.sh new file mode 100755 index 00000000..99608769 --- /dev/null +++ b/dnsapi/dns_dnshome.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env sh + +# dnsHome.de API for acme.sh +# +# This Script adds the necessary TXT record to a Subdomain +# +# Author dnsHome.de (https://github.com/dnsHome-de) +# +# Report Bugs to https://github.com/acmesh-official/acme.sh/issues/3819 +# +# export DNSHOME_Subdomain="" +# export DNSHOME_SubdomainPassword="" + +# Usage: add subdomain.ddnsdomain.tld "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +dns_dnshome_add() { + txtvalue=$2 + + DNSHOME_Subdomain="${DNSHOME_Subdomain:-$(_readdomainconf DNSHOME_Subdomain)}" + DNSHOME_SubdomainPassword="${DNSHOME_SubdomainPassword:-$(_readdomainconf DNSHOME_SubdomainPassword)}" + + if [ -z "$DNSHOME_Subdomain" ] || [ -z "$DNSHOME_SubdomainPassword" ]; then + DNSHOME_Subdomain="" + DNSHOME_SubdomainPassword="" + _err "Please specify/export your dnsHome.de Subdomain and Password" + return 1 + fi + + #save the credentials to the account conf file. + _savedomainconf DNSHOME_Subdomain "$DNSHOME_Subdomain" + _savedomainconf DNSHOME_SubdomainPassword "$DNSHOME_SubdomainPassword" + + DNSHOME_Api="https://$DNSHOME_Subdomain:$DNSHOME_SubdomainPassword@www.dnshome.de/dyndns.php" + + _DNSHOME_rest POST "acme=add&txt=$txtvalue" + if ! echo "$response" | grep 'successfully' >/dev/null; then + _err "Error" + _err "$response" + return 1 + fi + + return 0 +} + +# Usage: txtvalue +# Used to remove the txt record after validation +dns_dnshome_rm() { + txtvalue=$2 + + DNSHOME_Subdomain="${DNSHOME_Subdomain:-$(_readdomainconf DNSHOME_Subdomain)}" + DNSHOME_SubdomainPassword="${DNSHOME_SubdomainPassword:-$(_readdomainconf DNSHOME_SubdomainPassword)}" + + DNSHOME_Api="https://$DNSHOME_Subdomain:$DNSHOME_SubdomainPassword@www.dnshome.de/dyndns.php" + + if [ -z "$DNSHOME_Subdomain" ] || [ -z "$DNSHOME_SubdomainPassword" ]; then + DNSHOME_Subdomain="" + DNSHOME_SubdomainPassword="" + _err "Please specify/export your dnsHome.de Subdomain and Password" + return 1 + fi + + _DNSHOME_rest POST "acme=rm&txt=$txtvalue" + if ! echo "$response" | grep 'successfully' >/dev/null; then + _err "Error" + _err "$response" + return 1 + fi + + return 0 +} + +#################### Private functions below ################################## +_DNSHOME_rest() { + method=$1 + data="$2" + _debug "$data" + + _debug data "$data" + response="$(_post "$data" "$DNSHOME_Api" "" "$method")" + + if [ "$?" != "0" ]; then + _err "error $data" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_dnsimple.sh b/dnsapi/dns_dnsimple.sh index 0dd3918a..d831eb2b 100644 --- a/dnsapi/dns_dnsimple.sh +++ b/dnsapi/dns_dnsimple.sh @@ -152,7 +152,7 @@ _get_records() { sub_domain=$3 _debug "fetching txt records" - _dnsimple_rest GET "$account_id/zones/$domain/records?per_page=100" + _dnsimple_rest GET "$account_id/zones/$domain/records?per_page=5000&sort=id:desc" if ! _contains "$response" "\"id\":"; then _err "failed to retrieve records" diff --git a/dnsapi/dns_dnsservices.sh b/dnsapi/dns_dnsservices.sh new file mode 100755 index 00000000..008153a4 --- /dev/null +++ b/dnsapi/dns_dnsservices.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env sh + +#This file name is "dns_dnsservices.sh" +#Script for Danish DNS registra and DNS hosting provider https://dns.services + +#Author: Bjarke Bruun +#Report Bugs here: https://github.com/acmesh-official/acme.sh/issues/4152 + +# Global variable to connect to the DNS.Services API +DNSServices_API=https://dns.services/api + +######## Public functions ##################### + +#Usage: dns_dnsservices_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_dnsservices_add() { + fulldomain="$1" + txtvalue="$2" + + _info "Using dns.services to create ACME DNS challenge" + _debug2 add_fulldomain "$fulldomain" + _debug2 add_txtvalue "$txtvalue" + + # Read username/password from environment or .acme.sh/accounts.conf + DnsServices_Username="${DnsServices_Username:-$(_readaccountconf_mutable DnsServices_Username)}" + DnsServices_Password="${DnsServices_Password:-$(_readaccountconf_mutable DnsServices_Password)}" + if [ -z "$DnsServices_Username" ] || [ -z "$DnsServices_Password" ]; then + DnsServices_Username="" + DnsServices_Password="" + _err "You didn't specify dns.services api username and password yet." + _err "Set environment variables DnsServices_Username and DnsServices_Password" + return 1 + fi + + # Setup GET/POST/DELETE headers + _setup_headers + + #save the credentials to the account conf file. + _saveaccountconf_mutable DnsServices_Username "$DnsServices_Username" + _saveaccountconf_mutable DnsServices_Password "$DnsServices_Password" + + if ! _contains "$DnsServices_Username" "@"; then + _err "It seems that the username variable DnsServices_Username has not been set/left blank" + _err "or is not a valid email. Please correct and try again." + return 1 + fi + + if ! _get_root "${fulldomain}"; then + _err "Invalid domain ${fulldomain}" + return 1 + fi + + if ! createRecord "$fulldomain" "${txtvalue}"; then + _err "Error creating TXT record in domain $fulldomain in $rootZoneName" + return 1 + fi + + _debug2 challenge-created "Created $fulldomain" + return 0 +} + +#Usage: fulldomain txtvalue +#Description: Remove the txt record after validation. +dns_dnsservices_rm() { + fulldomain="$1" + txtvalue="$2" + + _info "Using dns.services to remove DNS record $fulldomain TXT $txtvalue" + _debug rm_fulldomain "$fulldomain" + _debug rm_txtvalue "$txtvalue" + + # Read username/password from environment or .acme.sh/accounts.conf + DnsServices_Username="${DnsServices_Username:-$(_readaccountconf_mutable DnsServices_Username)}" + DnsServices_Password="${DnsServices_Password:-$(_readaccountconf_mutable DnsServices_Password)}" + if [ -z "$DnsServices_Username" ] || [ -z "$DnsServices_Password" ]; then + DnsServices_Username="" + DnsServices_Password="" + _err "You didn't specify dns.services api username and password yet." + _err "Set environment variables DnsServices_Username and DnsServices_Password" + return 1 + fi + + # Setup GET/POST/DELETE headers + _setup_headers + + if ! _get_root "${fulldomain}"; then + _err "Invalid domain ${fulldomain}" + return 1 + fi + + _debug2 rm_rootDomainInfo "found root domain $rootZoneName for $fulldomain" + + if ! deleteRecord "${fulldomain}" "${txtvalue}"; then + _err "Error removing record: $fulldomain TXT ${txtvalue}" + return 1 + fi + + return 0 +} + +#################### Private functions below ################################## + +_setup_headers() { + # Set up API Headers for _get() and _post() + # The _add or _rm must have been called before to work + + if [ -z "$DnsServices_Username" ] || [ -z "$DnsServices_Password" ]; then + _err "Could not setup BASIC authentication headers, they are missing" + return 1 + fi + + DnsServiceCredentials="$(printf "%s" "$DnsServices_Username:$DnsServices_Password" | _base64)" + export _H1="Authorization: Basic $DnsServiceCredentials" + export _H2="Content-Type: application/json" + + # Just return if headers are set + return 0 +} + +_get_root() { + domain="$1" + _debug2 _get_root "Get the root domain of ${domain} for DNS API" + + # Setup _get() and _post() headers + #_setup_headers + + result=$(_H1="$_H1" _H2="$_H2" _get "$DNSServices_API/dns") + result2="$(printf "%s\n" "$result" | tr '[' '\n' | grep '"name"')" + result3="$(printf "%s\n" "$result2" | tr '}' '\n' | grep '"name"' | sed "s,^\,,,g" | sed "s,$,},g")" + useResult="" + _debug2 _get_root "Got the following root domain(s) $result" + _debug2 _get_root "- JSON: $result" + + if [ "$(printf "%s\n" "$result" | tr '}' '\n' | grep -c '"name"')" -gt "1" ]; then + checkMultiZones="true" + _debug2 _get_root "- multiple zones found" + else + checkMultiZones="false" + _debug2 _get_root "- single zone found" + fi + + # Find/isolate the root zone to work with in createRecord() and deleteRecord() + rootZone="" + if [ "$checkMultiZones" = "true" ]; then + #rootZone=$(for x in $(printf "%s" "${result3}" | tr ',' '\n' | sed -n 's/.*"name":"\(.*\)",.*/\1/p'); do if [ "$(echo "$domain" | grep "$x")" != "" ]; then echo "$x"; fi; done) + rootZone=$(for x in $(printf "%s\n" "${result3}" | tr ',' '\n' | grep name | cut -d'"' -f4); do if [ "$(echo "$domain" | grep "$x")" != "" ]; then echo "$x"; fi; done) + if [ "$rootZone" != "" ]; then + _debug2 _rootZone "- root zone for $domain is $rootZone" + else + _err "Could not find root zone for $domain, is it correctly typed?" + return 1 + fi + else + rootZone=$(echo "$result" | tr '}' '\n' | _egrep_o '"name":"[^"]*' | cut -d'"' -f4) + _debug2 _get_root "- only found 1 domain in API: $rootZone" + fi + + if [ -z "$rootZone" ]; then + _err "Could not find root domain for $domain - is it correctly typed?" + return 1 + fi + + # Make sure we use the correct API zone data + useResult="$(printf "%s\n" "${result3}" tr ',' '\n' | grep "$rootZone")" + _debug2 _useResult "useResult=$useResult" + + # Setup variables used by other functions to communicate with DNS.Services API + #zoneInfo=$(printf "%s\n" "$useResult" | sed -E 's,.*(zones)(.*),\1\2,g' | sed -E 's,^(.*"name":")([^"]*)"(.*)$,\2,g') + zoneInfo=$(printf "%s\n" "$useResult" | tr ',' '\n' | grep '"name"' | cut -d'"' -f4) + rootZoneName="$rootZone" + subDomainName="$(printf "%s\n" "$domain" | sed "s,\.$rootZone,,g")" + subDomainNameClean="$(printf "%s\n" "$domain" | sed "s,_acme-challenge.,,g")" + rootZoneDomainID=$(printf "%s\n" "$useResult" | tr ',' '\n' | grep domain_id | cut -d'"' -f4) + rootZoneServiceID=$(printf "%s\n" "$useResult" | tr ',' '\n' | grep service_id | cut -d'"' -f4) + + _debug2 _zoneInfo "Zone info from API : $zoneInfo" + _debug2 _get_root "Root zone name : $rootZoneName" + _debug2 _get_root "Root zone domain ID : $rootZoneDomainID" + _debug2 _get_root "Root zone service ID: $rootZoneServiceID" + _debug2 _get_root "Sub domain : $subDomainName" + + _debug _get_root "Found valid root domain $rootZone for $subDomainNameClean" + return 0 +} + +createRecord() { + fulldomain="$1" + txtvalue="$2" + + # Get root domain information - needed for DNS.Services API communication + if [ -z "$rootZoneName" ] || [ -z "$rootZoneDomainID" ] || [ -z "$rootZoneServiceID" ]; then + _get_root "$fulldomain" + fi + if [ -z "$rootZoneName" ] || [ -z "$rootZoneDomainID" ] || [ -z "$rootZoneServiceID" ]; then + _err "Something happend - could not get the API zone information" + return 1 + fi + + _debug2 createRecord "CNAME TXT value is: $txtvalue" + + # Prepare data to send to API + data="{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"${txtvalue}\", \"ttl\":\"10\"}" + + _debug2 createRecord "data to API: $data" + result=$(_post "$data" "$DNSServices_API/service/$rootZoneServiceID/dns/$rootZoneDomainID/records" "" "POST") + _debug2 createRecord "result from API: $result" + + if [ "$(echo "$result" | _egrep_o "\"success\":true")" = "" ]; then + _err "Failed to create TXT record $fulldomain with content $txtvalue in zone $rootZoneName" + _err "$result" + return 1 + fi + + _info "Record \"$fulldomain TXT $txtvalue\" has been created" + return 0 +} + +deleteRecord() { + fulldomain="$1" + txtvalue="$2" + + _log deleteRecord "Deleting $fulldomain TXT $txtvalue record" + + if [ -z "$rootZoneName" ] || [ -z "$rootZoneDomainID" ] || [ -z "$rootZoneServiceID" ]; then + _get_root "$fulldomain" + fi + + result="$(_H1="$_H1" _H2="$_H2" _get "$DNSServices_API/service/$rootZoneServiceID/dns/$rootZoneDomainID")" + #recordInfo="$(echo "$result" | sed -e 's/:{/:{\n/g' -e 's/},/\n},\n/g' | grep "${txtvalue}")" + #recordID="$(echo "$recordInfo" | sed -e 's/:{/:{\n/g' -e 's/},/\n},\n/g' | grep "${txtvalue}" | sed -E 's,.*(zones)(.*),\1\2,g' | sed -E 's,^(.*"id":")([^"]*)"(.*)$,\2,g')" + recordID="$(printf "%s\n" "$result" | tr '}' '\n' | grep -- "$txtvalue" | tr ',' '\n' | grep '"id"' | cut -d'"' -f4)" + _debug2 _recordID "recordID used for deletion of record: $recordID" + + if [ -z "$recordID" ]; then + _info "Record $fulldomain TXT $txtvalue not found or already deleted" + return 0 + else + _debug2 deleteRecord "Found recordID=$recordID" + fi + + _debug2 deleteRecord "DELETE request $DNSServices_API/service/$rootZoneServiceID/dns/$rootZoneDomainID/records/$recordID" + _log "curl DELETE request $DNSServices_API/service/$rootZoneServiceID/dns/$rootZoneDomainID/records/$recordID" + result="$(_H1="$_H1" _H2="$_H2" _post "" "$DNSServices_API/service/$rootZoneServiceID/dns/$rootZoneDomainID/records/$recordID" "" "DELETE")" + _debug2 deleteRecord "API Delete result \"$result\"" + _log "curl API Delete result \"$result\"" + + # Return OK regardless + return 0 +} diff --git a/dnsapi/dns_do.sh b/dnsapi/dns_do.sh deleted file mode 100755 index 3a2f8f49..00000000 --- a/dnsapi/dns_do.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env sh - -# DNS API for Domain-Offensive / Resellerinterface / Domainrobot - -# Report bugs at https://github.com/seidler2547/acme.sh/issues - -# set these environment variables to match your customer ID and password: -# DO_PID="KD-1234567" -# DO_PW="cdfkjl3n2" - -DO_URL="https://soap.resellerinterface.de/" - -######## Public functions ##################### - -#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_do_add() { - fulldomain=$1 - txtvalue=$2 - if _dns_do_authenticate; then - _info "Adding TXT record to ${_domain} as ${fulldomain}" - _dns_do_soap createRR origin "${_domain}" name "${fulldomain}" type TXT data "${txtvalue}" ttl 300 - if _contains "${response}" '>success<'; then - return 0 - fi - _err "Could not create resource record, check logs" - fi - return 1 -} - -#fulldomain -dns_do_rm() { - fulldomain=$1 - if _dns_do_authenticate; then - if _dns_do_list_rrs; then - _dns_do_had_error=0 - for _rrid in ${_rr_list}; do - _info "Deleting resource record $_rrid for $_domain" - _dns_do_soap deleteRR origin "${_domain}" rrid "${_rrid}" - if ! _contains "${response}" '>success<'; then - _dns_do_had_error=1 - _err "Could not delete resource record for ${_domain}, id ${_rrid}" - fi - done - return $_dns_do_had_error - fi - fi - return 1 -} - -#################### Private functions below ################################## -_dns_do_authenticate() { - _info "Authenticating as ${DO_PID}" - _dns_do_soap authPartner partner "${DO_PID}" password "${DO_PW}" - if _contains "${response}" '>success<'; then - _get_root "$fulldomain" - _debug "_domain $_domain" - return 0 - else - _err "Authentication failed, are DO_PID and DO_PW set correctly?" - fi - return 1 -} - -_dns_do_list_rrs() { - _dns_do_soap getRRList origin "${_domain}" - if ! _contains "${response}" 'SOAP-ENC:Array'; then - _err "getRRList origin ${_domain} failed" - return 1 - fi - _rr_list="$(echo "${response}" \ - | tr -d "\n\r\t" \ - | sed -e 's//\n/g' \ - | grep ">$(_regexcape "$fulldomain")" \ - | sed -e 's/<\/item>/\n/g' \ - | grep '>id[0-9]{1,16}<' \ - | tr -d '><')" - [ "${_rr_list}" ] -} - -_dns_do_soap() { - func="$1" - shift - # put the parameters to xml - body="" - while [ "$1" ]; do - _k="$1" - shift - _v="$1" - shift - body="$body<$_k>$_v" - done - body="$body" - _debug2 "SOAP request ${body}" - - # build SOAP XML - _xml=' - - '"$body"' -' - - # set SOAP headers - export _H1="SOAPAction: ${DO_URL}#${func}" - - if ! response="$(_post "${_xml}" "${DO_URL}")"; then - _err "Error <$1>" - return 1 - fi - _debug2 "SOAP response $response" - - # retrieve cookie header - _H2="$(_egrep_o 'Cookie: [^;]+' <"$HTTP_HEADER" | _head_n 1)" - export _H2 - - return 0 -} - -_get_root() { - domain=$1 - i=1 - - _dns_do_soap getDomainList - _all_domains="$(echo "${response}" \ - | tr -d "\n\r\t " \ - | _egrep_o 'domain]+>[^<]+' \ - | sed -e 's/^domain<\/key>]*>//g')" - - while true; do - h=$(printf "%s" "$domain" | cut -d . -f $i-100) - if [ -z "$h" ]; then - return 1 - fi - - if _contains "${_all_domains}" "^$(_regexcape "$h")\$"; then - _domain="$h" - return 0 - fi - - i=$(_math $i + 1) - done - _debug "$domain not found" - - return 1 -} - -_regexcape() { - echo "$1" | sed -e 's/\([]\.$*^[]\)/\\\1/g' -} diff --git a/dnsapi/dns_doapi.sh b/dnsapi/dns_doapi.sh new file mode 100755 index 00000000..a001d52c --- /dev/null +++ b/dnsapi/dns_doapi.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env sh + +# Official Let's Encrypt API for do.de / Domain-Offensive +# +# This is different from the dns_do adapter, because dns_do is only usable for enterprise customers +# This API is also available to private customers/individuals +# +# Provide the required LetsEncrypt token like this: +# DO_LETOKEN="FmD408PdqT1E269gUK57" + +DO_API="https://www.do.de/api/letsencrypt" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_doapi_add() { + fulldomain=$1 + txtvalue=$2 + + DO_LETOKEN="${DO_LETOKEN:-$(_readaccountconf_mutable DO_LETOKEN)}" + if [ -z "$DO_LETOKEN" ]; then + DO_LETOKEN="" + _err "You didn't configure a do.de API token yet." + _err "Please set DO_LETOKEN and try again." + return 1 + fi + _saveaccountconf_mutable DO_LETOKEN "$DO_LETOKEN" + + _info "Adding TXT record to ${fulldomain}" + response="$(_get "$DO_API?token=$DO_LETOKEN&domain=${fulldomain}&value=${txtvalue}")" + if _contains "${response}" 'success'; then + return 0 + fi + _err "Could not create resource record, check logs" + _err "${response}" + return 1 +} + +dns_doapi_rm() { + fulldomain=$1 + + DO_LETOKEN="${DO_LETOKEN:-$(_readaccountconf_mutable DO_LETOKEN)}" + if [ -z "$DO_LETOKEN" ]; then + DO_LETOKEN="" + _err "You didn't configure a do.de API token yet." + _err "Please set DO_LETOKEN and try again." + return 1 + fi + _saveaccountconf_mutable DO_LETOKEN "$DO_LETOKEN" + + _info "Deleting resource record $fulldomain" + response="$(_get "$DO_API?token=$DO_LETOKEN&domain=${fulldomain}&action=delete")" + if _contains "${response}" 'success'; then + return 0 + fi + _err "Could not delete resource record, check logs" + _err "${response}" + return 1 +} diff --git a/dnsapi/dns_domeneshop.sh b/dnsapi/dns_domeneshop.sh new file mode 100644 index 00000000..9a3791f4 --- /dev/null +++ b/dnsapi/dns_domeneshop.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env sh + +DOMENESHOP_Api_Endpoint="https://api.domeneshop.no/v0" + +##################### Public functions ##################### + +# Usage: dns_domeneshop_add +# Example: dns_domeneshop_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_domeneshop_add() { + fulldomain=$1 + txtvalue=$2 + + # Get token and secret + DOMENESHOP_Token="${DOMENESHOP_Token:-$(_readaccountconf_mutable DOMENESHOP_Token)}" + DOMENESHOP_Secret="${DOMENESHOP_Secret:-$(_readaccountconf_mutable DOMENESHOP_Secret)}" + + if [ -z "$DOMENESHOP_Token" ] || [ -z "$DOMENESHOP_Secret" ]; then + DOMENESHOP_Token="" + DOMENESHOP_Secret="" + _err "You need to spesify a Domeneshop/Domainnameshop API Token and Secret." + return 1 + fi + + # Save the api token and secret. + _saveaccountconf_mutable DOMENESHOP_Token "$DOMENESHOP_Token" + _saveaccountconf_mutable DOMENESHOP_Secret "$DOMENESHOP_Secret" + + # Get the domain name id + if ! _get_domainid "$fulldomain"; then + _err "Did not find domainname" + return 1 + fi + + # Create record + _domeneshop_rest POST "domains/$_domainid/dns" "{\"type\":\"TXT\",\"host\":\"$_sub_domain\",\"data\":\"$txtvalue\",\"ttl\":120}" +} + +# Usage: dns_domeneshop_rm +# Example: dns_domeneshop_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_domeneshop_rm() { + fulldomain=$1 + txtvalue=$2 + + # Get token and secret + DOMENESHOP_Token="${DOMENESHOP_Token:-$(_readaccountconf_mutable DOMENESHOP_Token)}" + DOMENESHOP_Secret="${DOMENESHOP_Secret:-$(_readaccountconf_mutable DOMENESHOP_Secret)}" + + if [ -z "$DOMENESHOP_Token" ] || [ -z "$DOMENESHOP_Secret" ]; then + DOMENESHOP_Token="" + DOMENESHOP_Secret="" + _err "You need to spesify a Domeneshop/Domainnameshop API Token and Secret." + return 1 + fi + + # Get the domain name id + if ! _get_domainid "$fulldomain"; then + _err "Did not find domainname" + return 1 + fi + + # Find record + if ! _get_recordid "$_domainid" "$_sub_domain" "$txtvalue"; then + _err "Did not find dns record" + return 1 + fi + + # Remove record + _domeneshop_rest DELETE "domains/$_domainid/dns/$_recordid" +} + +##################### Private functions ##################### + +_get_domainid() { + domain=$1 + + # Get domains + _domeneshop_rest GET "domains" + + if ! _contains "$response" "\"id\":"; then + _err "failed to get domain names" + return 1 + fi + + i=2 + 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 _contains "$response" "\"$h\"" >/dev/null; then + # We have found the domain name. + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + _domainid=$(printf "%s" "$response" | _egrep_o "[^{]*\"domain\":\"$_domain\"[^}]*" | _egrep_o "\"id\":[0-9]+" | cut -d : -f 2) + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_get_recordid() { + domainid=$1 + subdomain=$2 + txtvalue=$3 + + # Get all dns records for the domainname + _domeneshop_rest GET "domains/$domainid/dns" + + if ! _contains "$response" "\"id\":"; then + _debug "No records in dns" + return 1 + fi + + if ! _contains "$response" "\"host\":\"$subdomain\""; then + _debug "Record does not exist" + return 1 + fi + + # Get the id of the record in question + _recordid=$(printf "%s" "$response" | _egrep_o "[^{]*\"host\":\"$subdomain\"[^}]*" | _egrep_o "[^{]*\"data\":\"$txtvalue\"[^}]*" | _egrep_o "\"id\":[0-9]+" | cut -d : -f 2) + if [ -z "$_recordid" ]; then + return 1 + fi + return 0 +} + +_domeneshop_rest() { + method=$1 + endpoint=$2 + data=$3 + + credentials=$(printf "%b" "$DOMENESHOP_Token:$DOMENESHOP_Secret" | _base64) + + export _H1="Authorization: Basic $credentials" + export _H2="Content-Type: application/json" + + if [ "$method" != "GET" ]; then + response="$(_post "$data" "$DOMENESHOP_Api_Endpoint/$endpoint" "" "$method")" + else + response="$(_get "$DOMENESHOP_Api_Endpoint/$endpoint")" + fi + + if [ "$?" != "0" ]; then + _err "error $endpoint" + return 1 + fi + + return 0 +} diff --git a/dnsapi/dns_dp.sh b/dnsapi/dns_dp.sh index 3cc720aa..9b8b7a8b 100755 --- a/dnsapi/dns_dp.sh +++ b/dnsapi/dns_dp.sh @@ -53,7 +53,7 @@ dns_dp_rm() { return 1 fi - if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain"; then + if ! _rest POST "Record.List" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain_id=$_domain_id&sub_domain=$_sub_domain"; then _err "Record.Lis error." return 1 fi @@ -63,19 +63,19 @@ dns_dp_rm() { return 0 fi - record_id=$(echo "$response" | _egrep_o '{[^{]*"value":"'"$txtvalue"'"' | cut -d , -f 1 | cut -d : -f 2 | tr -d \") + record_id=$(echo "$response" | tr "{" "\n" | grep -- "$txtvalue" | grep '^"id"' | cut -d : -f 2 | cut -d '"' -f 2) _debug record_id "$record_id" if [ -z "$record_id" ]; then _err "Can not get record id." return 1 fi - if ! _rest POST "Record.Remove" "login_token=$DP_Id,$DP_Key&format=json&domain_id=$_domain_id&record_id=$record_id"; then + if ! _rest POST "Record.Remove" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain_id=$_domain_id&record_id=$record_id"; then _err "Record.Remove error." return 1 fi - _contains "$response" "Action completed successful" + _contains "$response" "successful" } @@ -89,11 +89,11 @@ add_record() { _info "Adding record" - if ! _rest POST "Record.Create" "login_token=$DP_Id,$DP_Key&format=json&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 - _contains "$response" "Action completed successful" || _contains "$response" "Domain record already exists" + _contains "$response" "successful" || _contains "$response" "Domain record already exists" } #################### Private functions below ################################## @@ -113,11 +113,11 @@ _get_root() { return 1 fi - if ! _rest POST "Domain.Info" "login_token=$DP_Id,$DP_Key&format=json&domain=$h"; then + if ! _rest POST "Domain.Info" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain=$h"; then return 1 fi - if _contains "$response" "Action completed successful"; then + if _contains "$response" "successful"; then _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \") _debug _domain_id "$_domain_id" if [ "$_domain_id" ]; then diff --git a/dnsapi/dns_dpi.sh b/dnsapi/dns_dpi.sh index 831150a9..2955effd 100755 --- a/dnsapi/dns_dpi.sh +++ b/dnsapi/dns_dpi.sh @@ -53,7 +53,7 @@ dns_dpi_rm() { return 1 fi - if ! _rest POST "Record.List" "user_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain"; then + if ! _rest POST "Record.List" "login_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain"; then _err "Record.Lis error." return 1 fi @@ -63,19 +63,19 @@ dns_dpi_rm() { return 0 fi - record_id=$(echo "$response" | _egrep_o '{[^{]*"value":"'"$txtvalue"'"' | cut -d , -f 1 | cut -d : -f 2 | tr -d \") + record_id=$(echo "$response" | tr "{" "\n" | grep -- "$txtvalue" | grep '^"id"' | cut -d : -f 2 | cut -d '"' -f 2) _debug record_id "$record_id" if [ -z "$record_id" ]; then _err "Can not get record id." return 1 fi - if ! _rest POST "Record.Remove" "user_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&record_id=$record_id"; then + if ! _rest POST "Record.Remove" "login_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&record_id=$record_id"; then _err "Record.Remove error." return 1 fi - _contains "$response" "Action completed successful" + _contains "$response" "Operation successful" } @@ -89,11 +89,11 @@ add_record() { _info "Adding record" - if ! _rest POST "Record.Create" "user_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=default"; then + if ! _rest POST "Record.Create" "login_token=$DPI_Id,$DPI_Key&format=json&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=default"; then return 1 fi - _contains "$response" "Action completed successful" || _contains "$response" "Domain record already exists" + _contains "$response" "Operation successful" || _contains "$response" "Domain record already exists" } #################### Private functions below ################################## @@ -113,11 +113,11 @@ _get_root() { return 1 fi - if ! _rest POST "Domain.Info" "user_token=$DPI_Id,$DPI_Key&format=json&domain=$h"; then + if ! _rest POST "Domain.Info" "login_token=$DPI_Id,$DPI_Key&format=json&domain=$h"; then return 1 fi - if _contains "$response" "Action completed successful"; then + if _contains "$response" "Operation successful"; then _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \") _debug _domain_id "$_domain_id" if [ "$_domain_id" ]; then diff --git a/dnsapi/dns_duckdns.sh b/dnsapi/dns_duckdns.sh index 711b81ee..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() { @@ -91,13 +91,12 @@ dns_duckdns_rm() { #################### Private functions below ################################## -#fulldomain=_acme-challenge.domain.duckdns.org -#returns -# _duckdns_domain=domain +# fulldomain may be 'domain.duckdns.org' (if using --domain-alias) or '_acme-challenge.domain.duckdns.org' +# either way, return 'domain'. (duckdns does not allow further subdomains and restricts domains to [a-z0-9-].) _duckdns_get_domain() { # We'll extract the domain/username from full domain - _duckdns_domain="$(printf "%s" "$fulldomain" | _lower_case | _egrep_o '[.][^.][^.]*[.]duckdns.org' | cut -d . -f 2)" + _duckdns_domain="$(printf "%s" "$fulldomain" | _lower_case | _egrep_o '^(_acme-challenge\.)?([a-z0-9-]+\.)+duckdns\.org' | sed -n 's/^\([^.]\{1,\}\.\)*\([a-z0-9-]\{1,\}\)\.duckdns\.org$/\2/p;')" if [ -z "$_duckdns_domain" ]; then _err "Error extracting the domain." @@ -113,16 +112,21 @@ _duckdns_rest() { param="$2" _debug param "$param" url="$DuckDNS_API?$param" + if [ -n "$DEBUG" ] && [ "$DEBUG" -gt 0 ]; then + url="$url&verbose=true" + fi _debug url "$url" # DuckDNS uses GET to update domain info if [ "$method" = "GET" ]; then response="$(_get "$url")" + _debug2 response "$response" + if [ -n "$DEBUG" ] && [ "$DEBUG" -gt 0 ] && _contains "$response" "UPDATED" && _contains "$response" "OK"; then + response="OK" + fi else _err "Unsupported method" return 1 fi - - _debug2 response "$response" return 0 } diff --git a/dnsapi/dns_durabledns.sh b/dnsapi/dns_durabledns.sh new file mode 100644 index 00000000..677ae24d --- /dev/null +++ b/dnsapi/dns_durabledns.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env sh + +#DD_API_User="xxxxx" +#DD_API_Key="xxxxxx" + +_DD_BASE="https://durabledns.com/services/dns" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_durabledns_add() { + fulldomain=$1 + txtvalue=$2 + + DD_API_User="${DD_API_User:-$(_readaccountconf_mutable DD_API_User)}" + DD_API_Key="${DD_API_Key:-$(_readaccountconf_mutable DD_API_Key)}" + if [ -z "$DD_API_User" ] || [ -z "$DD_API_Key" ]; then + DD_API_User="" + DD_API_Key="" + _err "You didn't specify a durabledns api user or key yet." + _err "You can get yours from here https://durabledns.com/dashboard/index.php" + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable DD_API_User "$DD_API_User" + _saveaccountconf_mutable DD_API_Key "$DD_API_Key" + + _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" + + _dd_soap createRecord string zonename "$_domain." string name "$_sub_domain" string type "TXT" string data "$txtvalue" int aux 0 int ttl 10 string ddns_enabled N + _contains "$response" "createRecordResponse" +} + +dns_durabledns_rm() { + fulldomain=$1 + txtvalue=$2 + + DD_API_User="${DD_API_User:-$(_readaccountconf_mutable DD_API_User)}" + DD_API_Key="${DD_API_Key:-$(_readaccountconf_mutable DD_API_Key)}" + if [ -z "$DD_API_User" ] || [ -z "$DD_API_Key" ]; then + DD_API_User="" + DD_API_Key="" + _err "You didn't specify a durabledns api user or key yet." + _err "You can get yours from here https://durabledns.com/dashboard/index.php" + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Find record id" + if ! _dd_soap listRecords string zonename "$_domain."; then + _err "can not listRecords" + return 1 + fi + + subtxt="$(echo "$txtvalue" | cut -c 1-30)" + record="$(echo "$response" | sed 's//#/g' | tr '#' '\n' | grep ">$subtxt")" + _debug record "$record" + if [ -z "$record" ]; then + _err "can not find record for txtvalue" "$txtvalue" + _err "$response" + return 1 + fi + + recordid="$(echo "$record" | _egrep_o '[0-9]*' | cut -d '>' -f 2 | cut -d '<' -f 1)" + _debug recordid "$recordid" + if [ -z "$recordid" ]; then + _err "can not find record id" + return 1 + fi + + if ! _dd_soap deleteRecord string zonename "$_domain." int id "$recordid"; then + _err "delete error" + return 1 + fi + + _contains "$response" "Success" +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + if ! _dd_soap "listZones"; then + return 1 + fi + + 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 _contains "$response" ">$h."; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 + +} + +#method +_dd_soap() { + _method="$1" + shift + _urn="${_method}wsdl" + # put the parameters to xml + body=" + $DD_API_User + $DD_API_Key + " + while [ "$1" ]; do + _t="$1" + shift + _k="$1" + shift + _v="$1" + shift + body="$body<$_k xsi:type=\"xsd:$_t\">$_v" + done + body="$body" + _debug2 "SOAP request ${body}" + + # build SOAP XML + _xml=' + + '"$body"' +' + + _debug2 _xml "$_xml" + # set SOAP headers + _action="SOAPAction: \"urn:$_urn#$_method\"" + _debug2 "_action" "$_action" + export _H1="$_action" + export _H2="Content-Type: text/xml; charset=utf-8" + + _url="$_DD_BASE/$_method.php" + _debug "_url" "$_url" + if ! response="$(_post "${_xml}" "${_url}")"; then + _err "Error <$1>" + return 1 + fi + _debug2 "response" "$response" + response="$(echo "$response" | tr -d "\r\n" | _egrep_o ":${_method}Response .*:${_method}Response><")" + _debug2 "response" "$response" + return 0 +} diff --git a/dnsapi/dns_dynu.sh b/dnsapi/dns_dynu.sh index 17a1cdb0..406ef17d 100644 --- a/dnsapi/dns_dynu.sh +++ b/dnsapi/dns_dynu.sh @@ -10,7 +10,7 @@ Dynu_Token="" # #Endpoint -Dynu_EndPoint="https://api.dynu.com/v1" +Dynu_EndPoint="https://api.dynu.com/v2" # #Author: Dynu Systems, Inc. #Report Bugs here: https://github.com/shar0119/acme.sh @@ -51,11 +51,11 @@ dns_dynu_add() { _debug _domain_name "$_domain_name" _info "Creating TXT record." - if ! _dynu_rest POST "dns/record/add" "{\"domain_name\":\"$_domain_name\",\"node_name\":\"$_node\",\"record_type\":\"TXT\",\"text_data\":\"$txtvalue\",\"state\":true,\"ttl\":90}"; then + if ! _dynu_rest POST "dns/$dnsId/record" "{\"domainId\":\"$dnsId\",\"nodeName\":\"$_node\",\"recordType\":\"TXT\",\"textData\":\"$txtvalue\",\"state\":true,\"ttl\":90}"; then return 1 fi - if ! _contains "$response" "text_data"; then + if ! _contains "$response" "200"; then _err "Could not add TXT record." return 1 fi @@ -132,11 +132,12 @@ _get_root() { return 1 fi - if ! _dynu_rest GET "dns/get/$h"; then + if ! _dynu_rest GET "dns/getroot/$h"; then return 1 fi - if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + if _contains "$response" "\"domainName\":\"$h\"" >/dev/null; then + dnsId=$(printf "%s" "$response" | tr -d "{}" | cut -d , -f 2 | cut -d : -f 2) _domain_name=$h _node=$(printf "%s" "$domain" | cut -d . -f 1-$p) return 0 @@ -152,7 +153,7 @@ _get_recordid() { fulldomain=$1 txtvalue=$2 - if ! _dynu_rest GET "dns/record/get?hostname=$fulldomain&rrtype=TXT"; then + if ! _dynu_rest GET "dns/$dnsId/record"; then return 1 fi @@ -161,19 +162,18 @@ _get_recordid() { return 0 fi - _dns_record_id=$(printf "%s" "$response" | _egrep_o "{[^}]*}" | grep "\"text_data\":\"$txtvalue\"" | _egrep_o ",[^,]*," | grep ',"id":' | tr -d ",," | cut -d : -f 2) - + _dns_record_id=$(printf "%s" "$response" | sed -e 's/[^{]*\({[^}]*}\)[^{]*/\1\n/g' | grep "\"textData\":\"$txtvalue\"" | sed -e 's/.*"id":\([^,]*\).*/\1/') return 0 } _delete_txt_record() { _dns_record_id=$1 - if ! _dynu_rest GET "dns/record/delete/$_dns_record_id"; then + if ! _dynu_rest DELETE "dns/$dnsId/record/$_dns_record_id"; then return 1 fi - if ! _contains "$response" "true"; then + if ! _contains "$response" "200"; then return 1 fi @@ -189,7 +189,7 @@ _dynu_rest() { export _H1="Authorization: Bearer $Dynu_Token" export _H2="Content-Type: application/json" - if [ "$data" ]; then + if [ "$data" ] || [ "$m" = "DELETE" ]; then _debug data "$data" response="$(_post "$data" "$Dynu_EndPoint/$ep" "" "$m")" else @@ -216,8 +216,12 @@ _dynu_authentication() { _err "Authentication failed." return 1 fi - if _contains "$response" "accessToken"; then - Dynu_Token=$(printf "%s" "$response" | tr -d "[]" | cut -d , -f 2 | cut -d : -f 2 | cut -d '"' -f 2) + if _contains "$response" "Authentication Exception"; then + _err "Authentication failed." + return 1 + fi + if _contains "$response" "access_token"; then + Dynu_Token=$(printf "%s" "$response" | tr -d "{}" | cut -d , -f 1 | cut -d : -f 2 | cut -d '"' -f 2) fi if _contains "$Dynu_Token" "null"; then Dynu_Token="" diff --git a/dnsapi/dns_dynv6.sh b/dnsapi/dns_dynv6.sh new file mode 100644 index 00000000..90814b1b --- /dev/null +++ b/dnsapi/dns_dynv6.sh @@ -0,0 +1,285 @@ +#!/usr/bin/env sh +#Author StefanAbl +#Usage specify a private keyfile to use with dynv6 'export KEY="path/to/keyfile"' +#or use the HTTP REST API by by specifying a token 'export DYNV6_TOKEN="value" +#if no keyfile is specified, you will be asked if you want to create one in /home/$USER/.ssh/dynv6 and /home/$USER/.ssh/dynv6.pub + +dynv6_api="https://dynv6.com/api/v2" +######## Public functions ##################### +# Please Read this guide first: https://github.com/Neilpang/acme.sh/wiki/DNS-API-Dev-Guide +#Usage: dns_dynv6_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_dynv6_add() { + fulldomain=$1 + txtvalue=$2 + _info "Using dynv6 api" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + _get_authentication + if [ "$dynv6_token" ]; then + _dns_dynv6_add_http + return $? + else + _info "using key file $dynv6_keyfile" + _your_hosts="$(ssh -i "$dynv6_keyfile" api@dynv6.com hosts)" + if ! _get_domain "$fulldomain" "$_your_hosts"; then + _err "Host not found on your account" + return 1 + fi + _debug "found host on your account" + returnval="$(ssh -i "$dynv6_keyfile" api@dynv6.com hosts \""$_host"\" records set \""$_record"\" txt data \""$txtvalue"\")" + _debug "Dynv6 returned this after record was added: $returnval" + if _contains "$returnval" "created"; then + return 0 + elif _contains "$returnval" "updated"; then + return 0 + else + _err "Something went wrong! it does not seem like the record was added successfully" + return 1 + fi + return 1 + fi + return 1 +} +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_dynv6_rm() { + fulldomain=$1 + txtvalue=$2 + _info "Using dynv6 API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + _get_authentication + if [ "$dynv6_token" ]; then + _dns_dynv6_rm_http + return $? + else + _info "using key file $dynv6_keyfile" + _your_hosts="$(ssh -i "$dynv6_keyfile" api@dynv6.com hosts)" + if ! _get_domain "$fulldomain" "$_your_hosts"; then + _err "Host not found on your account" + return 1 + fi + _debug "found host on your account" + _info "$(ssh -i "$dynv6_keyfile" api@dynv6.com hosts "\"$_host\"" records del "\"$_record\"" txt)" + return 0 + fi +} +#################### Private functions below ################################## +#Usage: No Input required +#returns +#dynv6_keyfile the path to the new key file that has been generated +_generate_new_key() { + dynv6_keyfile="$(eval echo ~"$USER")/.ssh/dynv6" + _info "Path to key file used: $dynv6_keyfile" + if [ ! -f "$dynv6_keyfile" ] && [ ! -f "$dynv6_keyfile.pub" ]; then + _debug "generating key in $dynv6_keyfile and $dynv6_keyfile.pub" + ssh-keygen -f "$dynv6_keyfile" -t ssh-ed25519 -N '' + else + _err "There is already a file in $dynv6_keyfile or $dynv6_keyfile.pub" + return 1 + fi +} + +#Usage: _acme-challenge.www.example.dynv6.net "$_your_hosts" +#where _your_hosts is the output of ssh -i ~/.ssh/dynv6.pub api@dynv6.com hosts +#returns +#_host= example.dynv6.net +#_record=_acme-challenge.www +#aborts if not a valid domain +_get_domain() { + #_your_hosts="$(ssh -i ~/.ssh/dynv6.pub api@dynv6.com hosts)" + _full_domain="$1" + _your_hosts="$2" + + _your_hosts="$(echo "$_your_hosts" | awk '/\./ {print $1}')" + for l in $_your_hosts; do + #echo "host: $l" + if test "${_full_domain#*"$l"}" != "$_full_domain"; then + _record=${_full_domain%."$l"} + _host=$l + _debug "The host is $_host and the record $_record" + return 0 + fi + done + _err "Either their is no such host on your dnyv6 account or it cannot be accessed with this key" + return 1 +} + +# Usage: No input required +#returns +#dynv6_keyfile path to the key that will be used +_get_authentication() { + dynv6_token="${DYNV6_TOKEN:-$(_readaccountconf_mutable dynv6_token)}" + if [ "$dynv6_token" ]; then + _debug "Found HTTP Token. Going to use the HTTP API and not the SSH API" + if [ "$DYNV6_TOKEN" ]; then + _saveaccountconf_mutable dynv6_token "$dynv6_token" + fi + else + _debug "no HTTP token found. Looking for an SSH key" + dynv6_keyfile="${dynv6_keyfile:-$(_readaccountconf_mutable dynv6_keyfile)}" + _debug "Your key is $dynv6_keyfile" + if [ -z "$dynv6_keyfile" ]; then + if [ -z "$KEY" ]; then + _err "You did not specify a key to use with dynv6" + _info "Creating new dynv6 API key to add to dynv6.com" + _generate_new_key + _info "Please add this key to dynv6.com $(cat "$dynv6_keyfile.pub")" + _info "Hit Enter to continue" + read -r _ + #save the credentials to the account conf file. + else + dynv6_keyfile="$KEY" + fi + _saveaccountconf_mutable dynv6_keyfile "$dynv6_keyfile" + fi + fi +} + +_dns_dynv6_add_http() { + _debug "Got HTTP token form _get_authentication method. Going to use the HTTP API" + if ! _get_zone_id "$fulldomain"; then + _err "Could not find a matching zone for $fulldomain. Maybe your HTTP Token is not authorized to access the zone" + return 1 + fi + _get_zone_name "$_zone_id" + record=${fulldomain%%."$_zone_name"} + _set_record TXT "$record" "$txtvalue" + if _contains "$response" "$txtvalue"; then + _info "Successfully added record" + return 0 + else + _err "Something went wrong while adding the record" + return 1 + fi +} + +_dns_dynv6_rm_http() { + _debug "Got HTTP token form _get_authentication method. Going to use the HTTP API" + if ! _get_zone_id "$fulldomain"; then + _err "Could not find a matching zone for $fulldomain. Maybe your HTTP Token is not authorized to access the zone" + return 1 + fi + _get_zone_name "$_zone_id" + record=${fulldomain%%."$_zone_name"} + _get_record_id "$_zone_id" "$record" "$txtvalue" + _del_record "$_zone_id" "$_record_id" + if [ -z "$response" ]; then + _info "Successfully deleted record" + return 0 + else + _err "Something went wrong while deleting the record" + return 1 + fi +} + +#get the zoneid for a specifc record or zone +#usage: _get_zone_id §record +#where $record is the record to get the id for +#returns _zone_id the id of the zone +_get_zone_id() { + record="$1" + _debug "getting zone id for $record" + _dynv6_rest GET zones + + zones="$(echo "$response" | tr '}' '\n' | tr ',' '\n' | grep name | sed 's/\[//g' | tr -d '{' | tr -d '"')" + #echo $zones + + selected="" + for z in $zones; do + z="${z#name:}" + _debug zone: "$z" + if _contains "$record" "$z"; then + _debug "$z found in $record" + selected="$z" + fi + done + if [ -z "$selected" ]; then + _err "no zone found" + return 1 + fi + + zone_id="$(echo "$response" | tr '}' '\n' | grep "$selected" | tr ',' '\n' | grep id | tr -d '"')" + _zone_id="${zone_id#id:}" + _debug "zone id: $_zone_id" +} + +_get_zone_name() { + _zone_id="$1" + _dynv6_rest GET zones/"$_zone_id" + _zone_name="$(echo "$response" | tr ',' '\n' | tr -d '{' | grep name | tr -d '"')" + _zone_name="${_zone_name#name:}" +} + +#usaage _get_record_id $zone_id $record +# where zone_id is thevalue returned by _get_zone_id +# and record ist in the form _acme.www for an fqdn of _acme.www.example.com +# returns _record_id +_get_record_id() { + _zone_id="$1" + record="$2" + value="$3" + _dynv6_rest GET "zones/$_zone_id/records" + if ! _get_record_id_from_response "$response"; then + _err "no such record $record found in zone $_zone_id" + return 1 + fi +} + +_get_record_id_from_response() { + response="$1" + _record_id="$(echo "$response" | tr '}' '\n' | grep "\"name\":\"$record\"" | grep "\"data\":\"$value\"" | tr ',' '\n' | grep id | tr -d '"' | tr -d 'id:')" + #_record_id="${_record_id#id:}" + if [ -z "$_record_id" ]; then + _err "no such record: $record found in zone $_zone_id" + return 1 + fi + _debug "record id: $_record_id" + return 0 +} +#usage: _set_record TXT _acme_challenge.www longvalue 12345678 +#zone id is optional can also be set as vairable bevor calling this method +_set_record() { + type="$1" + record="$2" + value="$3" + if [ "$4" ]; then + _zone_id="$4" + fi + data="{\"name\": \"$record\", \"data\": \"$value\", \"type\": \"$type\"}" + #data='{ "name": "acme.test.thorn.dynv6.net", "type": "A", "data": "192.168.0.1"}' + echo "$data" + #"{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}" + _dynv6_rest POST "zones/$_zone_id/records" "$data" +} +_del_record() { + _zone_id=$1 + _record_id=$2 + _dynv6_rest DELETE zones/"$_zone_id"/records/"$_record_id" +} + +_dynv6_rest() { + m=$1 #method GET,POST,DELETE or PUT + ep="$2" #the endpoint + data="$3" + _debug "$ep" + + token_trimmed=$(echo "$dynv6_token" | tr -d '"') + + export _H1="Authorization: Bearer $token_trimmed" + export _H2="Content-Type: application/json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$dynv6_api/$ep" "" "$m")" + else + response="$(_get "$dynv6_api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_easydns.sh b/dnsapi/dns_easydns.sh new file mode 100644 index 00000000..ab47a0bc --- /dev/null +++ b/dnsapi/dns_easydns.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env sh + +####################################################### +# +# easyDNS REST API for acme.sh by Neilpang based on dns_cf.sh +# +# API Documentation: https://sandbox.rest.easydns.net:3001/ +# +# Author: wurzelpanzer [wurzelpanzer@maximolider.net] +# Report Bugs here: https://github.com/acmesh-official/acme.sh/issues/2647 +# +#################### Public functions ################# + +#EASYDNS_Key="xxxxxxxxxxxxxxxxxxxxxxxx" +#EASYDNS_Token="xxxxxxxxxxxxxxxxxxxxxxxx" +EASYDNS_Api="https://rest.easydns.net" + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_easydns_add() { + fulldomain=$1 + txtvalue=$2 + + EASYDNS_Token="${EASYDNS_Token:-$(_readaccountconf_mutable EASYDNS_Token)}" + EASYDNS_Key="${EASYDNS_Key:-$(_readaccountconf_mutable EASYDNS_Key)}" + + if [ -z "$EASYDNS_Token" ] || [ -z "$EASYDNS_Key" ]; then + _err "You didn't specify an easydns.net token or api key. Signup at https://cp.easydns.com/manage/security/api/signup.php" + return 1 + else + _saveaccountconf_mutable EASYDNS_Token "$EASYDNS_Token" + _saveaccountconf_mutable EASYDNS_Key "$EASYDNS_Key" + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _EASYDNS_rest GET "zones/records/all/${_domain}/search/${_sub_domain}" + + if ! printf "%s" "$response" | grep \"status\":200 >/dev/null; then + _err "Error" + return 1 + fi + + _info "Adding record" + if _EASYDNS_rest PUT "zones/records/add/$_domain/TXT" "{\"host\":\"$_sub_domain\",\"rdata\":\"$txtvalue\"}"; then + if _contains "$response" "\"status\":201"; then + _info "Added, OK" + return 0 + elif _contains "$response" "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_easydns_rm() { + fulldomain=$1 + txtvalue=$2 + + EASYDNS_Token="${EASYDNS_Token:-$(_readaccountconf_mutable EASYDNS_Token)}" + EASYDNS_Key="${EASYDNS_Key:-$(_readaccountconf_mutable EASYDNS_Key)}" + + _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" + _EASYDNS_rest GET "zones/records/all/${_domain}/search/${_sub_domain}" + + if ! printf "%s" "$response" | grep \"status\":200 >/dev/null; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | 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 ! _EASYDNS_rest DELETE "zones/records/$_domain/$record_id"; then + _err "Delete record error." + return 1 + fi + _contains "$response" "\"status\":200" + fi + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _EASYDNS_rest GET "zones/records/all/$h"; then + return 1 + fi + + if _contains "$response" "\"status\":200"; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_EASYDNS_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + basicauth=$(printf "%s" "$EASYDNS_Token":"$EASYDNS_Key" | _base64) + + export _H1="accept: application/json" + if [ "$basicauth" ]; then + export _H2="Authorization: Basic $basicauth" + fi + + if [ "$m" != "GET" ]; then + export _H3="Content-Type: application/json" + _debug data "$data" + response="$(_post "$data" "$EASYDNS_Api/$ep" "" "$m")" + else + response="$(_get "$EASYDNS_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_edgedns.sh b/dnsapi/dns_edgedns.sh new file mode 100755 index 00000000..27650eb1 --- /dev/null +++ b/dnsapi/dns_edgedns.sh @@ -0,0 +1,470 @@ +#!/usr/bin/env sh + +# Akamai Edge DNS v2 API +# User must provide Open Edgegrid API credentials to the EdgeDNS installation. The remote user in EdgeDNS must have CRUD access to +# Edge DNS Zones and Recordsets, e.g. DNS—Zone Record Management authorization + +# Report bugs to https://control.akamai.com/apps/support-ui/#/contact-support + +# Values to export: +# --EITHER-- +# *** TBD. NOT IMPLEMENTED YET *** +# specify Edgegrid credentials file and section +# AKAMAI_EDGERC= +# AKAMAI_EDGERC_SECTION="default" +## --OR-- +# specify indiviual credentials +# export AKAMAI_HOST = +# export AKAMAI_ACCESS_TOKEN = +# export AKAMAI_CLIENT_TOKEN = +# export AKAMAI_CLIENT_SECRET = + +ACME_EDGEDNS_VERSION="0.1.0" + +######## Public functions ##################### + +# Usage: dns_edgedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +# +dns_edgedns_add() { + fulldomain=$1 + txtvalue=$2 + _debug "ENTERING DNS_EDGEDNS_ADD" + _debug2 "fulldomain" "$fulldomain" + _debug2 "txtvalue" "$txtvalue" + + if ! _EDGEDNS_credentials; then + _err "$@" + return 1 + fi + if ! _EDGEDNS_getZoneInfo "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + _debug2 "Add: zone" "$zone" + acmeRecordURI=$(printf "%s/%s/names/%s/types/TXT" "$edge_endpoint" "$zone" "$fulldomain") + _debug3 "Add URL" "$acmeRecordURI" + # Get existing TXT record + _edge_result=$(_edgedns_rest GET "$acmeRecordURI") + _api_status="$?" + _debug3 "_edge_result" "$_edge_result" + if [ "$_api_status" -ne 0 ]; then + if [ "$curResult" = "FATAL" ]; then + _err "$(printf "Fatal error: acme API function call : %s" "$retVal")" + fi + if [ "$_edge_result" != "404" ]; then + _err "$(printf "Failure accessing Akamai Edge DNS API Server. Error: %s" "$_edge_result")" + return 1 + fi + fi + rdata="\"${txtvalue}\"" + record_op="POST" + if [ "$_api_status" -eq 0 ]; then + # record already exists. Get existing record data and update + record_op="PUT" + rdlist="${_edge_result#*\"rdata\":[}" + rdlist="${rdlist%%]*}" + rdlist=$(echo "$rdlist" | tr -d '"' | tr -d "\\\\") + _debug3 "existing TXT found" + _debug3 "record data" "$rdlist" + # value already there? + if _contains "$rdlist" "$txtvalue"; then + return 0 + fi + _txt_val="" + while [ "$_txt_val" != "$rdlist" ] && [ "${rdlist}" ]; do + _txt_val="${rdlist%%,*}" + rdlist="${rdlist#*,}" + rdata="${rdata},\"${_txt_val}\"" + done + fi + # Add the txtvalue TXT Record + body="{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":600, \"rdata\":"[${rdata}]"}" + _debug3 "Add body '${body}'" + _edge_result=$(_edgedns_rest "$record_op" "$acmeRecordURI" "$body") + _api_status="$?" + if [ "$_api_status" -eq 0 ]; then + _log "$(printf "Text value %s added to recordset %s" "$txtvalue" "$fulldomain")" + return 0 + else + _err "$(printf "error adding TXT record for validation. Error: %s" "$_edge_result")" + return 1 + fi +} + +# Usage: dns_edgedns_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to delete txt record +# +dns_edgedns_rm() { + fulldomain=$1 + txtvalue=$2 + _debug "ENTERING DNS_EDGEDNS_RM" + _debug2 "fulldomain" "$fulldomain" + _debug2 "txtvalue" "$txtvalue" + + if ! _EDGEDNS_credentials; then + _err "$@" + return 1 + fi + if ! _EDGEDNS_getZoneInfo "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + _debug2 "RM: zone" "${zone}" + acmeRecordURI=$(printf "%s/%s/names/%s/types/TXT" "${edge_endpoint}" "$zone" "$fulldomain") + _debug3 "RM URL" "$acmeRecordURI" + # Get existing TXT record + _edge_result=$(_edgedns_rest GET "$acmeRecordURI") + _api_status="$?" + if [ "$_api_status" -ne 0 ]; then + if [ "$curResult" = "FATAL" ]; then + _err "$(printf "Fatal error: acme API function call : %s" "$retVal")" + fi + if [ "$_edge_result" != "404" ]; then + _err "$(printf "Failure accessing Akamai Edge DNS API Server. Error: %s" "$_edge_result")" + return 1 + fi + fi + _debug3 "_edge_result" "$_edge_result" + record_op="DELETE" + body="" + if [ "$_api_status" -eq 0 ]; then + # record already exists. Get existing record data and update + rdlist="${_edge_result#*\"rdata\":[}" + rdlist="${rdlist%%]*}" + rdlist=$(echo "$rdlist" | tr -d '"' | tr -d "\\\\") + _debug3 "rdlist" "$rdlist" + if [ -n "$rdlist" ]; then + record_op="PUT" + comma="" + rdata="" + _txt_val="" + while [ "$_txt_val" != "$rdlist" ] && [ "$rdlist" ]; do + _txt_val="${rdlist%%,*}" + rdlist="${rdlist#*,}" + _debug3 "_txt_val" "$_txt_val" + _debug3 "txtvalue" "$txtvalue" + if ! _contains "$_txt_val" "$txtvalue"; then + rdata="${rdata}${comma}\"${_txt_val}\"" + comma="," + fi + done + if [ -z "$rdata" ]; then + record_op="DELETE" + else + # Recreate the txtvalue TXT Record + body="{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":600, \"rdata\":"[${rdata}]"}" + _debug3 "body" "$body" + fi + fi + fi + _edge_result=$(_edgedns_rest "$record_op" "$acmeRecordURI" "$body") + _api_status="$?" + if [ "$_api_status" -eq 0 ]; then + _log "$(printf "Text value %s removed from recordset %s" "$txtvalue" "$fulldomain")" + return 0 + else + _err "$(printf "error removing TXT record for validation. Error: %s" "$_edge_result")" + return 1 + fi +} + +#################### Private functions below ################################## + +_EDGEDNS_credentials() { + _debug "GettingEdge DNS credentials" + _log "$(printf "ACME DNSAPI Edge DNS version %s" ${ACME_EDGEDNS_VERSION})" + args_missing=0 + AKAMAI_ACCESS_TOKEN="${AKAMAI_ACCESS_TOKEN:-$(_readaccountconf_mutable AKAMAI_ACCESS_TOKEN)}" + if [ -z "$AKAMAI_ACCESS_TOKEN" ]; then + AKAMAI_ACCESS_TOKEN="" + AKAMAI_CLIENT_TOKEN="" + AKAMAI_HOST="" + AKAMAI_CLIENT_SECRET="" + _err "AKAMAI_ACCESS_TOKEN is missing" + args_missing=1 + fi + AKAMAI_CLIENT_TOKEN="${AKAMAI_CLIENT_TOKEN:-$(_readaccountconf_mutable AKAMAI_CLIENT_TOKEN)}" + if [ -z "$AKAMAI_CLIENT_TOKEN" ]; then + AKAMAI_ACCESS_TOKEN="" + AKAMAI_CLIENT_TOKEN="" + AKAMAI_HOST="" + AKAMAI_CLIENT_SECRET="" + _err "AKAMAI_CLIENT_TOKEN is missing" + args_missing=1 + fi + AKAMAI_HOST="${AKAMAI_HOST:-$(_readaccountconf_mutable AKAMAI_HOST)}" + if [ -z "$AKAMAI_HOST" ]; then + AKAMAI_ACCESS_TOKEN="" + AKAMAI_CLIENT_TOKEN="" + AKAMAI_HOST="" + AKAMAI_CLIENT_SECRET="" + _err "AKAMAI_HOST is missing" + args_missing=1 + fi + AKAMAI_CLIENT_SECRET="${AKAMAI_CLIENT_SECRET:-$(_readaccountconf_mutable AKAMAI_CLIENT_SECRET)}" + if [ -z "$AKAMAI_CLIENT_SECRET" ]; then + AKAMAI_ACCESS_TOKEN="" + AKAMAI_CLIENT_TOKEN="" + AKAMAI_HOST="" + AKAMAI_CLIENT_SECRET="" + _err "AKAMAI_CLIENT_SECRET is missing" + args_missing=1 + fi + + if [ "$args_missing" = 1 ]; then + _err "You have not properly specified the EdgeDNS Open Edgegrid API credentials. Please try again." + return 1 + else + _saveaccountconf_mutable AKAMAI_ACCESS_TOKEN "$AKAMAI_ACCESS_TOKEN" + _saveaccountconf_mutable AKAMAI_CLIENT_TOKEN "$AKAMAI_CLIENT_TOKEN" + _saveaccountconf_mutable AKAMAI_HOST "$AKAMAI_HOST" + _saveaccountconf_mutable AKAMAI_CLIENT_SECRET "$AKAMAI_CLIENT_SECRET" + # Set whether curl should use secure or insecure mode + fi + export HTTPS_INSECURE=0 # All Edgegrid API calls are secure + edge_endpoint=$(printf "https://%s/config-dns/v2/zones" "$AKAMAI_HOST") + _debug3 "Edge API Endpoint:" "$edge_endpoint" + +} + +_EDGEDNS_getZoneInfo() { + _debug "Getting Zoneinfo" + zoneEnd=false + curZone=$1 + while [ -n "$zoneEnd" ]; do + # we can strip the first part of the fulldomain, since its just the _acme-challenge string + curZone="${curZone#*.}" + # suffix . needed for zone -> domain.tld. + # create zone get url + get_zone_url=$(printf "%s/%s" "$edge_endpoint" "$curZone") + _debug3 "Zone Get: " "${get_zone_url}" + curResult=$(_edgedns_rest GET "$get_zone_url") + retVal=$? + if [ "$retVal" -ne 0 ]; then + if [ "$curResult" = "FATAL" ]; then + _err "$(printf "Fatal error: acme API function call : %s" "$retVal")" + fi + if [ "$curResult" != "404" ]; then + _err "$(printf "Managed zone validation failed. Error response: %s" "$retVal")" + return 1 + fi + fi + if _contains "$curResult" "\"zone\":"; then + _debug2 "Zone data" "${curResult}" + zone=$(echo "${curResult}" | _egrep_o "\"zone\"\\s*:\\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"") + _debug3 "Zone" "${zone}" + zoneEnd="" + return 0 + fi + + if [ "${curZone#*.}" != "$curZone" ]; then + _debug3 "$(printf "%s still contains a '.' - so we can check next higher level" "$curZone")" + else + zoneEnd=true + _err "Couldn't retrieve zone data." + return 1 + fi + done + _err "Failed to retrieve zone data." + return 2 +} + +_edgedns_headers="" + +_edgedns_rest() { + _debug "Handling API Request" + m=$1 + # Assume endpoint is complete path, including query args if applicable + ep=$2 + body_data=$3 + _edgedns_content_type="" + _request_url_path="$ep" + _request_body="$body_data" + _request_method="$m" + _edgedns_headers="" + tab="" + _edgedns_headers="${_edgedns_headers}${tab}Host: ${AKAMAI_HOST}" + tab="\t" + # Set in acme.sh _post/_get + #_edgedns_headers="${_edgedns_headers}${tab}User-Agent:ACME DNSAPI Edge DNS version ${ACME_EDGEDNS_VERSION}" + _edgedns_headers="${_edgedns_headers}${tab}Accept: application/json,*/*" + if [ "$m" != "GET" ] && [ "$m" != "DELETE" ]; then + _edgedns_content_type="application/json" + _debug3 "_request_body" "$_request_body" + _body_len=$(echo "$_request_body" | tr -d "\n\r" | awk '{print length}') + _edgedns_headers="${_edgedns_headers}${tab}Content-Length: ${_body_len}" + fi + _edgedns_make_auth_header + _edgedns_headers="${_edgedns_headers}${tab}Authorization: ${_signed_auth_header}" + _secure_debug2 "Made Auth Header" "$_signed_auth_header" + hdr_indx=1 + work_header="${_edgedns_headers}${tab}" + _debug3 "work_header" "$work_header" + while [ "$work_header" ]; do + entry="${work_header%%\\t*}" + work_header="${work_header#*\\t}" + export "$(printf "_H%s=%s" "$hdr_indx" "$entry")" + _debug2 "Request Header " "$entry" + hdr_indx=$((hdr_indx + 1)) + done + + # clear headers from previous request to avoid getting wrong http code on timeouts + : >"$HTTP_HEADER" + _debug2 "$ep" + if [ "$m" != "GET" ]; then + _debug3 "Method data" "$data" + # body url [needbase64] [POST|PUT|DELETE] [ContentType] + response=$(_post "$_request_body" "$ep" false "$m" "$_edgedns_content_type") + else + response=$(_get "$ep") + fi + _ret="$?" + if [ "$_ret" -ne 0 ]; then + _err "$(printf "acme.sh API function call failed. Error: %s" "$_ret")" + echo "FATAL" + return "$_ret" + fi + _debug2 "response" "${response}" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug2 "http response code" "$_code" + if [ "$_code" = "200" ] || [ "$_code" = "201" ]; then + # All good + response="$(echo "${response}" | _normalizeJson)" + echo "$response" + return 0 + fi + + if [ "$_code" = "204" ]; then + # Success, no body + echo "$_code" + return 0 + fi + + if [ "$_code" = "400" ]; then + _err "Bad request presented" + _log "$(printf "Headers: %s" "$_edgedns_headers")" + _log "$(printf "Method: %s" "$_request_method")" + _log "$(printf "URL: %s" "$ep")" + _log "$(printf "Data: %s" "$data")" + fi + + if [ "$_code" = "403" ]; then + _err "access denied make sure your Edgegrid cedentials are correct." + fi + + echo "$_code" + return 1 +} + +_edgedns_eg_timestamp() { + _debug "Generating signature Timestamp" + _debug3 "Retriving ntp time" + _timeheaders="$(_get "https://www.ntp.org" "onlyheader")" + _debug3 "_timeheaders" "$_timeheaders" + _ntpdate="$(echo "$_timeheaders" | grep -i "Date:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n")" + _debug3 "_ntpdate" "$_ntpdate" + _ntpdate="$(echo "${_ntpdate}" | sed -e 's/^[[:space:]]*//')" + _debug3 "_NTPDATE" "$_ntpdate" + _ntptime="$(echo "${_ntpdate}" | _head_n 1 | cut -d " " -f 5 | tr -d "\r\n")" + _debug3 "_ntptime" "$_ntptime" + _eg_timestamp=$(date -u "+%Y%m%dT") + _eg_timestamp="$(printf "%s%s+0000" "$_eg_timestamp" "$_ntptime")" + _debug "_eg_timestamp" "$_eg_timestamp" +} + +_edgedns_new_nonce() { + _debug "Generating Nonce" + _nonce=$(echo "EDGEDNS$(_time)" | _digest sha1 hex | cut -c 1-32) + _debug3 "_nonce" "$_nonce" +} + +_edgedns_make_auth_header() { + _debug "Constructing Auth Header" + _edgedns_new_nonce + _edgedns_eg_timestamp + # "Unsigned authorization header: 'EG1-HMAC-SHA256 client_token=block;access_token=block;timestamp=20200806T14:16:33+0000;nonce=72cde72c-82d9-4721-9854-2ba057929d67;'" + _auth_header="$(printf "EG1-HMAC-SHA256 client_token=%s;access_token=%s;timestamp=%s;nonce=%s;" "$AKAMAI_CLIENT_TOKEN" "$AKAMAI_ACCESS_TOKEN" "$_eg_timestamp" "$_nonce")" + _secure_debug2 "Unsigned Auth Header: " "$_auth_header" + + _edgedns_sign_request + _signed_auth_header="$(printf "%ssignature=%s" "$_auth_header" "$_signed_req")" + _secure_debug2 "Signed Auth Header: " "${_signed_auth_header}" +} + +_edgedns_sign_request() { + _debug2 "Signing http request" + _edgedns_make_data_to_sign "$_auth_header" + _secure_debug2 "Returned signed data" "$_mdata" + _edgedns_make_signing_key "$_eg_timestamp" + _edgedns_base64_hmac_sha256 "$_mdata" "$_signing_key" + _signed_req="$_hmac_out" + _secure_debug2 "Signed Request" "$_signed_req" +} + +_edgedns_make_signing_key() { + _debug2 "Creating sigining key" + ts=$1 + _edgedns_base64_hmac_sha256 "$ts" "$AKAMAI_CLIENT_SECRET" + _signing_key="$_hmac_out" + _secure_debug2 "Signing Key" "$_signing_key" + +} + +_edgedns_make_data_to_sign() { + _debug2 "Processing data to sign" + hdr=$1 + _secure_debug2 "hdr" "$hdr" + _edgedns_make_content_hash + path="$(echo "$_request_url_path" | tr -d "\n\r" | sed 's/https\?:\/\///')" + path=${path#*"$AKAMAI_HOST"} + _debug "hier path" "$path" + # dont expose headers to sign so use MT string + _mdata="$(printf "%s\thttps\t%s\t%s\t%s\t%s\t%s" "$_request_method" "$AKAMAI_HOST" "$path" "" "$_hash" "$hdr")" + _secure_debug2 "Data to Sign" "$_mdata" +} + +_edgedns_make_content_hash() { + _debug2 "Generating content hash" + _hash="" + _debug2 "Request method" "${_request_method}" + if [ "$_request_method" != "POST" ] || [ -z "$_request_body" ]; then + return 0 + fi + _debug2 "Req body" "$_request_body" + _edgedns_base64_sha256 "$_request_body" + _hash="$_sha256_out" + _debug2 "Content hash" "$_hash" +} + +_edgedns_base64_hmac_sha256() { + _debug2 "Generating hmac" + data=$1 + key=$2 + encoded_data="$(echo "$data" | iconv -t utf-8)" + encoded_key="$(echo "$key" | iconv -t utf-8)" + _secure_debug2 "encoded data" "$encoded_data" + _secure_debug2 "encoded key" "$encoded_key" + + encoded_key_hex=$(printf "%s" "$encoded_key" | _hex_dump | tr -d ' ') + data_sig="$(echo "$encoded_data" | tr -d "\n\r" | _hmac sha256 "$encoded_key_hex" | _base64)" + + _secure_debug2 "data_sig:" "$data_sig" + _hmac_out="$(echo "$data_sig" | tr -d "\n\r" | iconv -f utf-8)" + _secure_debug2 "hmac" "$_hmac_out" +} + +_edgedns_base64_sha256() { + _debug2 "Creating sha256 digest" + trg=$1 + _secure_debug2 "digest data" "$trg" + digest="$(echo "$trg" | tr -d "\n\r" | _digest "sha256")" + _sha256_out="$(echo "$digest" | tr -d "\n\r" | iconv -f utf-8)" + _secure_debug2 "digest decode" "$_sha256_out" +} + +#_edgedns_parse_edgerc() { +# filepath=$1 +# section=$2 +#} diff --git a/dnsapi/dns_euserv.sh b/dnsapi/dns_euserv.sh index 38101565..cfb4b814 100644 --- a/dnsapi/dns_euserv.sh +++ b/dnsapi/dns_euserv.sh @@ -127,7 +127,7 @@ dns_euserv_rm() { else # find XML block where txtvalue is in. The record_id is allways prior this line! _endLine=$(echo "$response" | grep -n '>dns_record_content<.*>'"$txtvalue"'<' | cut -d ':' -f 1) - # record_id is the last Tag with a number before the row _endLine, identified by + # record_id is the last Tag with a number before the row _endLine, identified by _record_id=$(echo "$response" | sed -n '1,'"$_endLine"'p' | grep '' | _tail_n 1 | sed 's/.*\([0-9]*\)<\/name>.*/\1/') _info "Deleting record" _euserv_delete_record "$_record_id" diff --git a/dnsapi/dns_exoscale.sh b/dnsapi/dns_exoscale.sh new file mode 100755 index 00000000..ccf05fc5 --- /dev/null +++ b/dnsapi/dns_exoscale.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env sh + +EXOSCALE_API=https://api.exoscale.com/dns/v1 + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +dns_exoscale_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _checkAuth; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + if _exoscale_rest POST "domains/$_domain_id/records" "{\"record\":{\"name\":\"$_sub_domain\",\"record_type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}}" "$_domain_token"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + fi + fi + _err "Add txt record error." + return 1 + +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +dns_exoscale_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _checkAuth; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _exoscale_rest GET "domains/${_domain_id}/records?type=TXT&name=$_sub_domain" "" "$_domain_token" + if _contains "$response" "\"name\":\"$_sub_domain\"" >/dev/null; then + _record_id=$(echo "$response" | tr '{' "\n" | grep "\"content\":\"$txtvalue\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \") + fi + + if [ -z "$_record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + + _debug "Deleting record $_record_id" + + if ! _exoscale_rest DELETE "domains/$_domain_id/records/$_record_id" "" "$_domain_token"; then + _err "Delete record error." + return 1 + fi + + return 0 +} + +#################### Private functions below ################################## + +_checkAuth() { + EXOSCALE_API_KEY="${EXOSCALE_API_KEY:-$(_readaccountconf_mutable EXOSCALE_API_KEY)}" + EXOSCALE_SECRET_KEY="${EXOSCALE_SECRET_KEY:-$(_readaccountconf_mutable EXOSCALE_SECRET_KEY)}" + + if [ -z "$EXOSCALE_API_KEY" ] || [ -z "$EXOSCALE_SECRET_KEY" ]; then + EXOSCALE_API_KEY="" + EXOSCALE_SECRET_KEY="" + _err "You don't specify Exoscale application key and application secret yet." + _err "Please create you key and try again." + return 1 + fi + + _saveaccountconf_mutable EXOSCALE_API_KEY "$EXOSCALE_API_KEY" + _saveaccountconf_mutable EXOSCALE_SECRET_KEY "$EXOSCALE_SECRET_KEY" + + return 0 +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +# _domain_token=sdjkglgdfewsdfg +_get_root() { + + if ! _exoscale_rest GET "domains"; then + return 1 + fi + + domain=$1 + i=2 + 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 _contains "$response" "\"name\":\"$h\"" >/dev/null; then + _domain_id=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \") + _domain_token=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + if [ "$_domain_token" ] && [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +# returns response +_exoscale_rest() { + method=$1 + path="$2" + data="$3" + token="$4" + request_url="$EXOSCALE_API/$path" + _debug "$path" + + export _H1="Accept: application/json" + + if [ "$token" ]; then + export _H2="X-DNS-Domain-Token: $token" + else + export _H2="X-DNS-Token: $EXOSCALE_API_KEY:$EXOSCALE_SECRET_KEY" + fi + + if [ "$data" ] || [ "$method" = "DELETE" ]; then + export _H3="Content-Type: application/json" + _debug data "$data" + response="$(_post "$data" "$request_url" "" "$method")" + else + response="$(_get "$request_url" "" "" "$method")" + fi + + if [ "$?" != "0" ]; then + _err "error $request_url" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_fornex.sh b/dnsapi/dns_fornex.sh new file mode 100644 index 00000000..53be307a --- /dev/null +++ b/dnsapi/dns_fornex.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env sh + +#Author: Timur Umarov + +FORNEX_API_URL="https://fornex.com/api/dns/v0.1" + +######## Public functions ##################### + +#Usage: dns_fornex_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_fornex_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _Fornex_API; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Unable to determine root domain" + return 1 + else + _debug _domain "$_domain" + fi + + _info "Adding record" + if _rest POST "$_domain/entry_set/add/" "host=$fulldomain&type=TXT&value=$txtvalue&apikey=$FORNEX_API_KEY"; then + _debug _response "$response" + if _contains "$response" '"ok": true' || _contains "$response" 'Такая запись уже существует.'; then + _info "Added, OK" + return 0 + fi + fi + _err "Add txt record error." + return 1 +} + +#Usage: dns_fornex_rm _acme-challenge.www.domain.com +dns_fornex_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _Fornex_API; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Unable to determine root domain" + return 1 + else + _debug _domain "$_domain" + fi + + _debug "Getting txt records" + _rest GET "$_domain/entry_set.json?apikey=$FORNEX_API_KEY" + + if ! _contains "$response" "$txtvalue"; then + _err "Txt record not found" + return 1 + fi + + _record_id="$(echo "$response" | _egrep_o "{[^{]*\"value\"*:*\"$txtvalue\"[^}]*}" | sed -n -e 's#.*"id": \([0-9]*\).*#\1#p')" + _debug "_record_id" "$_record_id" + if [ -z "$_record_id" ]; then + _err "can not find _record_id" + return 1 + fi + + if ! _rest POST "$_domain/entry_set/$_record_id/delete/" "apikey=$FORNEX_API_KEY"; then + _err "Delete record error." + return 1 + fi + return 0 +} + +#################### 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 + #not valid + return 1 + fi + + if ! _rest GET "domain_list.json?q=$h&apikey=$FORNEX_API_KEY"; then + return 1 + fi + + if _contains "$response" "\"$h\"" >/dev/null; then + _domain=$h + return 0 + else + _debug "$h not found" + fi + i=$(_math "$i" + 1) + done + + return 1 +} + +_Fornex_API() { + FORNEX_API_KEY="${FORNEX_API_KEY:-$(_readaccountconf_mutable FORNEX_API_KEY)}" + if [ -z "$FORNEX_API_KEY" ]; then + FORNEX_API_KEY="" + + _err "You didn't specify the Fornex API key yet." + _err "Please create your key and try again." + + return 1 + fi + + _saveaccountconf_mutable FORNEX_API_KEY "$FORNEX_API_KEY" +} + +#method method action data +_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Accept: application/json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$FORNEX_API_URL/$ep" "" "$m")" + else + response="$(_get "$FORNEX_API_URL/$ep" | _normalizeJson)" + fi + + _ret="$?" + if [ "$_ret" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_freedns.sh b/dnsapi/dns_freedns.sh index 7262755e..29cee430 100755 --- a/dnsapi/dns_freedns.sh +++ b/dnsapi/dns_freedns.sh @@ -7,6 +7,7 @@ # #Author: David Kerr #Report Bugs here: https://github.com/dkerr64/acme.sh +#or here... https://github.com/acmesh-official/acme.sh/issues/2305 # ######## Public functions ##################### @@ -46,76 +47,34 @@ dns_freedns_add() { _saveaccountconf FREEDNS_COOKIE "$FREEDNS_COOKIE" - # split our full domain name into two parts... - i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)" - i="$(_math "$i" - 1)" - top_domain="$(echo "$fulldomain" | cut -d. -f "$i"-100)" - i="$(_math "$i" - 1)" - sub_domain="$(echo "$fulldomain" | cut -d. -f -"$i")" + # We may have to cycle through the domain name to find the + # TLD that we own... + i=1 + wmax="$(echo "$fulldomain" | tr '.' ' ' | wc -w)" + while [ "$i" -lt "$wmax" ]; do + # split our full domain name into two parts... + sub_domain="$(echo "$fulldomain" | cut -d. -f -"$i")" + i="$(_math "$i" + 1)" + top_domain="$(echo "$fulldomain" | cut -d. -f "$i"-100)" + _debug "sub_domain: $sub_domain" + _debug "top_domain: $top_domain" - _debug "top_domain: $top_domain" - _debug "sub_domain: $sub_domain" - - # Sometimes FreeDNS does not return the subdomain page but rather - # returns a page regarding becoming a premium member. This usually - # happens after a period of inactivity. Immediately trying again - # returns the correct subdomain page. So, we will try twice to - # load the page and obtain our domain ID - attempts=2 - while [ "$attempts" -gt "0" ]; do - attempts="$(_math "$attempts" - 1)" - - htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" - if [ "$?" != "0" ]; then - if [ "$using_cached_cookies" = "true" ]; then - _err "Has your FreeDNS username and password changed? If so..." - _err "Please export as FREEDNS_User / FREEDNS_Password and try again." - fi - return 1 - fi - - subdomain_csv="$(echo "$htmlpage" | tr -d "\n\r" | _egrep_o '
' | sed 's//@/g' | tr '@' '\n' | grep edit.php | grep "$top_domain")" - _debug3 "subdomain_csv: $subdomain_csv" - - # The above beauty ends with striping out rows that do not have an - # href to edit.php and do not have the top domain we are looking for. - # So all we should be left with is CSV of table of subdomains we are - # interested in. - - # Now we have to read through this table and extract the data we need - lines="$(echo "$subdomain_csv" | wc -l)" - i=0 - found=0 - DNSdomainid="" - while [ "$i" -lt "$lines" ]; do - i="$(_math "$i" + 1)" - line="$(echo "$subdomain_csv" | sed -n "${i}p")" - _debug2 "line: $line" - if [ $found = 0 ] && _contains "$line" "$top_domain"; then - # this line will contain DNSdomainid for the top_domain - DNSdomainid="$(echo "$line" | _egrep_o "edit_domain_id *= *.*>" | cut -d = -f 2 | cut -d '>' -f 1)" - _debug2 "DNSdomainid: $DNSdomainid" - found=1 - break - fi - done - - if [ -z "$DNSdomainid" ]; then - # If domain ID is empty then something went wrong (top level - # domain not found at FreeDNS). - if [ "$attempts" = "0" ]; then - # exhausted maximum retry attempts - _err "Domain $top_domain not found at FreeDNS" - return 1 - fi - else - # break out of the 'retry' loop... we have found our domain ID + DNSdomainid="$(_freedns_domain_id "$top_domain")" + if [ "$?" = "0" ]; then + _info "Domain $top_domain found at FreeDNS, domain_id $DNSdomainid" break + else + _info "Domain $top_domain not found at FreeDNS, try with next level of TLD" fi - _info "Domain $top_domain not found at FreeDNS" - _info "Retry loading subdomain page ($attempts attempts remaining)" done + if [ -z "$DNSdomainid" ]; then + # If domain ID is empty then something went wrong (top level + # domain not found at FreeDNS). + _err "Domain $top_domain not found at FreeDNS" + return 1 + fi + # Add in new TXT record with the value provided _debug "Adding TXT record for $fulldomain, $txtvalue" _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue" @@ -134,80 +93,47 @@ dns_freedns_rm() { # Need to read cookie from conf file again in case new value set # during login to FreeDNS when TXT record was created. - # acme.sh does not have a _readaccountconf() function - FREEDNS_COOKIE="$(_read_conf "$ACCOUNT_CONF_PATH" "FREEDNS_COOKIE")" + FREEDNS_COOKIE="$(_readaccountconf "FREEDNS_COOKIE")" _debug "FreeDNS login cookies: $FREEDNS_COOKIE" - # Sometimes FreeDNS does not return the subdomain page but rather - # returns a page regarding becoming a premium member. This usually - # happens after a period of inactivity. Immediately trying again - # returns the correct subdomain page. So, we will try twice to - # load the page and obtain our TXT record. - attempts=2 - while [ "$attempts" -gt "0" ]; do - attempts="$(_math "$attempts" - 1)" + TXTdataid="$(_freedns_data_id "$fulldomain" "TXT")" + if [ "$?" != "0" ]; then + _info "Cannot delete TXT record for $fulldomain, record does not exist at FreeDNS" + return 1 + fi + _debug "Data ID's found, $TXTdataid" - htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" + # now we have one (or more) TXT record data ID's. Load the page + # for that record and search for the record txt value. If match + # then we can delete it. + lines="$(echo "$TXTdataid" | wc -l)" + _debug "Found $lines TXT data records for $fulldomain" + i=0 + while [ "$i" -lt "$lines" ]; do + i="$(_math "$i" + 1)" + dataid="$(echo "$TXTdataid" | sed -n "${i}p")" + _debug "$dataid" + + htmlpage="$(_freedns_retrieve_data_page "$FREEDNS_COOKIE" "$dataid")" if [ "$?" != "0" ]; then + if [ "$using_cached_cookies" = "true" ]; then + _err "Has your FreeDNS username and password changed? If so..." + _err "Please export as FREEDNS_User / FREEDNS_Password and try again." + fi return 1 fi - subdomain_csv="$(echo "$htmlpage" | tr -d "\n\r" | _egrep_o '' | sed 's//@/g' | tr '@' '\n' | grep edit.php | grep "$fulldomain")" - _debug3 "subdomain_csv: $subdomain_csv" - - # The above beauty ends with striping out rows that do not have an - # href to edit.php and do not have the domain name we are looking for. - # So all we should be left with is CSV of table of subdomains we are - # interested in. - - # Now we have to read through this table and extract the data we need - lines="$(echo "$subdomain_csv" | wc -l)" - i=0 - found=0 - DNSdataid="" - while [ "$i" -lt "$lines" ]; do - i="$(_math "$i" + 1)" - line="$(echo "$subdomain_csv" | sed -n "${i}p")" - _debug3 "line: $line" - DNSname="$(echo "$line" | _egrep_o 'edit.php.*' | cut -d '>' -f 2 | cut -d '<' -f 1)" - _debug2 "DNSname: $DNSname" - if [ "$DNSname" = "$fulldomain" ]; then - DNStype="$(echo "$line" | sed 's/' -f 2 | cut -d '<' -f 1)" - _debug2 "DNStype: $DNStype" - if [ "$DNStype" = "TXT" ]; then - DNSdataid="$(echo "$line" | _egrep_o 'data_id=.*' | cut -d = -f 2 | cut -d '>' -f 1)" - _debug2 "DNSdataid: $DNSdataid" - DNSvalue="$(echo "$line" | sed 's/' -f 2 | cut -d '<' -f 1)" - if _startswith "$DNSvalue" """; then - # remove the quotation from the start - DNSvalue="$(echo "$DNSvalue" | cut -c 7-)" - fi - if _endswith "$DNSvalue" "..."; then - # value was truncated, remove the dot dot dot from the end - DNSvalue="$(echo "$DNSvalue" | sed 's/...$//')" - elif _endswith "$DNSvalue" """; then - # else remove the closing quotation from the end - DNSvalue="$(echo "$DNSvalue" | sed 's/......$//')" - fi - _debug2 "DNSvalue: $DNSvalue" - - if [ -n "$DNSdataid" ] && _startswith "$txtvalue" "$DNSvalue"; then - # Found a match. But note... Website is truncating the - # value field so we are only testing that part that is not - # truncated. This should be accurate enough. - _debug "Deleting TXT record for $fulldomain, $txtvalue" - _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid" - return $? - fi - - fi - fi - done + echo "$htmlpage" | grep "value=\""$txtvalue"\"" >/dev/null + if [ "$?" = "0" ]; then + # Found a match... delete the record and return + _info "Deleting TXT record for $fulldomain, $txtvalue" + _freedns_delete_txt_record "$FREEDNS_COOKIE" "$dataid" + return $? + fi done - # If we get this far we did not find a match (after two attempts) + # If we get this far we did not find a match # Not necessarily an error, but log anyway. - _debug3 "$subdomain_csv" _info "Cannot delete TXT record for $fulldomain, $txtvalue. Does not exist at FreeDNS" return 0 } @@ -271,6 +197,33 @@ _freedns_retrieve_subdomain_page() { return 0 } +# usage _freedns_retrieve_data_page login_cookies data_id +# echo page retrieved (html) +# returns 0 success +_freedns_retrieve_data_page() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + data_id="$2" + url="https://freedns.afraid.org/subdomain/edit.php?data_id=$2" + + _debug "Retrieve data page for ID $data_id from FreeDNS" + + htmlpage="$(_get "$url")" + + if [ "$?" != "0" ]; then + _err "FreeDNS retrieve data page failed bad RC from _get" + return 1 + elif [ -z "$htmlpage" ]; then + _err "FreeDNS returned empty data page" + return 1 + fi + + _debug3 "htmlpage: $htmlpage" + + printf "%s" "$htmlpage" + return 0 +} + # usage _freedns_add_txt_record login_cookies domain_id subdomain value # returns 0 success _freedns_add_txt_record() { @@ -324,3 +277,95 @@ _freedns_delete_txt_record() { _info "Deleted acme challenge TXT record for $fulldomain at FreeDNS" return 0 } + +# usage _freedns_domain_id domain_name +# echo the domain_id if found +# return 0 success +_freedns_domain_id() { + # Start by escaping the dots in the domain name + search_domain="$(echo "$1" | sed 's/\./\\./g')" + + # Sometimes FreeDNS does not return the subdomain page but rather + # returns a page regarding becoming a premium member. This usually + # happens after a period of inactivity. Immediately trying again + # returns the correct subdomain page. So, we will try twice to + # load the page and obtain our domain ID + attempts=2 + while [ "$attempts" -gt "0" ]; do + attempts="$(_math "$attempts" - 1)" + + htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" + if [ "$?" != "0" ]; then + if [ "$using_cached_cookies" = "true" ]; then + _err "Has your FreeDNS username and password changed? If so..." + _err "Please export as FREEDNS_User / FREEDNS_Password and try again." + fi + return 1 + fi + + domain_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's//@/g' | tr '@' '\n' | + grep "$search_domain\|$search_domain(.*)" | + sed -n 's/.*\(edit\.php?edit_domain_id=[0-9a-zA-Z]*\).*/\1/p' | + cut -d = -f 2)" + # The above beauty extracts domain ID from the html page... + # strip out all blank space and new lines. Then insert newlines + # before each table row + # search for the domain within each row (which may or may not have + # a text string in brackets (.*) after it. + # And finally extract the domain ID. + if [ -n "$domain_id" ]; then + printf "%s" "$domain_id" + return 0 + fi + _debug "Domain $search_domain not found. Retry loading subdomain page ($attempts attempts remaining)" + done + _debug "Domain $search_domain not found after retry" + return 1 +} + +# usage _freedns_data_id domain_name record_type +# echo the data_id(s) if found +# return 0 success +_freedns_data_id() { + # Start by escaping the dots in the domain name + search_domain="$(echo "$1" | sed 's/\./\\./g')" + record_type="$2" + + # Sometimes FreeDNS does not return the subdomain page but rather + # returns a page regarding becoming a premium member. This usually + # happens after a period of inactivity. Immediately trying again + # returns the correct subdomain page. So, we will try twice to + # load the page and obtain our domain ID + attempts=2 + while [ "$attempts" -gt "0" ]; do + attempts="$(_math "$attempts" - 1)" + + htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" + if [ "$?" != "0" ]; then + if [ "$using_cached_cookies" = "true" ]; then + _err "Has your FreeDNS username and password changed? If so..." + _err "Please export as FREEDNS_User / FREEDNS_Password and try again." + fi + return 1 + fi + + data_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's//@/g' | tr '@' '\n' | + grep "$record_type" | + grep "$search_domain" | + sed -n 's/.*\(edit\.php?data_id=[0-9a-zA-Z]*\).*/\1/p' | + cut -d = -f 2)" + # The above beauty extracts data ID from the html page... + # strip out all blank space and new lines. Then insert newlines + # before each table row + # search for the record type withing each row (e.g. TXT) + # search for the domain within each row (which is within a + # anchor. And finally extract the domain ID. + if [ -n "$data_id" ]; then + printf "%s" "$data_id" + return 0 + fi + _debug "Domain $search_domain not found. Retry loading subdomain page ($attempts attempts remaining)" + done + _debug "Domain $search_domain not found after retry" + return 1 +} diff --git a/dnsapi/dns_gandi_livedns.sh b/dnsapi/dns_gandi_livedns.sh old mode 100755 new mode 100644 index 7a21aba6..14939d7c --- a/dnsapi/dns_gandi_livedns.sh +++ b/dnsapi/dns_gandi_livedns.sh @@ -1,12 +1,14 @@ #!/usr/bin/env sh # Gandi LiveDNS v5 API -# http://doc.livedns.gandi.net/ +# https://api.gandi.net/docs/livedns/ +# https://api.gandi.net/docs/authentication/ for token + apikey (deprecated) authentication # currently under beta # # Requires GANDI API KEY set in GANDI_LIVEDNS_KEY set as environment variable # #Author: Frédéric Crozat +# Dominik Röttsches #Report Bugs here: https://github.com/fcrozat/acme.sh # ######## Public functions ##################### @@ -18,13 +20,20 @@ dns_gandi_livedns_add() { fulldomain=$1 txtvalue=$2 - if [ -z "$GANDI_LIVEDNS_KEY" ]; then - _err "No API key specified for Gandi LiveDNS." - _err "Create your key and export it as GANDI_LIVEDNS_KEY" + if [ -z "$GANDI_LIVEDNS_KEY" ] && [ -z "$GANDI_LIVEDNS_TOKEN" ]; then + _err "No Token or API key (deprecated) specified for Gandi LiveDNS." + _err "Create your token or key and export it as GANDI_LIVEDNS_KEY or GANDI_LIVEDNS_TOKEN respectively" return 1 fi - _saveaccountconf GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY" + # Keep only one secret in configuration + if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then + _saveaccountconf GANDI_LIVEDNS_TOKEN "$GANDI_LIVEDNS_TOKEN" + _clearaccountconf GANDI_LIVEDNS_KEY + elif [ -n "$GANDI_LIVEDNS_KEY" ]; then + _saveaccountconf GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY" + _clearaccountconf GANDI_LIVEDNS_TOKEN + fi _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -36,9 +45,7 @@ dns_gandi_livedns_add() { _debug domain "$_domain" _debug sub_domain "$_sub_domain" - _gandi_livedns_rest PUT "domains/$_domain/records/$_sub_domain/TXT" "{\"rrset_ttl\": 300, \"rrset_values\":[\"$txtvalue\"]}" \ - && _contains "$response" '{"message": "DNS Record Created"}' \ - && _info "Add $(__green "success")" + _dns_gandi_append_record "$_domain" "$_sub_domain" "$txtvalue" } #Usage: fulldomain txtvalue @@ -56,9 +63,23 @@ dns_gandi_livedns_rm() { _debug fulldomain "$fulldomain" _debug domain "$_domain" _debug sub_domain "$_sub_domain" + _debug txtvalue "$txtvalue" - _gandi_livedns_rest DELETE "domains/$_domain/records/$_sub_domain/TXT" "" + if ! _dns_gandi_existing_rrset_values "$_domain" "$_sub_domain"; then + return 1 + fi + _new_rrset_values=$(echo "$_rrset_values" | sed "s/...$txtvalue...//g") + # Cleanup dangling commata. + _new_rrset_values=$(echo "$_new_rrset_values" | sed "s/, ,/ ,/g") + _new_rrset_values=$(echo "$_new_rrset_values" | sed "s/, *\]/\]/g") + _new_rrset_values=$(echo "$_new_rrset_values" | sed "s/\[ *,/\[/g") + _debug "New rrset_values" "$_new_rrset_values" + _gandi_livedns_rest PUT \ + "domains/$_domain/records/$_sub_domain/TXT" \ + "{\"rrset_ttl\": 300, \"rrset_values\": $_new_rrset_values}" && + _contains "$response" '{"message": "DNS Record Created"}' && + _info "Removing record $(__green "success")" } #################### Private functions below ################################## @@ -98,6 +119,45 @@ _get_root() { return 1 } +_dns_gandi_append_record() { + domain=$1 + sub_domain=$2 + txtvalue=$3 + + if _dns_gandi_existing_rrset_values "$domain" "$sub_domain"; then + _debug "Appending new value" + _rrset_values=$(echo "$_rrset_values" | sed "s/\"]/\",\"$txtvalue\"]/") + else + _debug "Creating new record" "$_rrset_values" + _rrset_values="[\"$txtvalue\"]" + fi + _debug new_rrset_values "$_rrset_values" + _gandi_livedns_rest PUT "domains/$_domain/records/$sub_domain/TXT" \ + "{\"rrset_ttl\": 300, \"rrset_values\": $_rrset_values}" && + _contains "$response" '{"message": "DNS Record Created"}' && + _info "Adding record $(__green "success")" +} + +_dns_gandi_existing_rrset_values() { + domain=$1 + sub_domain=$2 + if ! _gandi_livedns_rest GET "domains/$domain/records/$sub_domain"; then + return 1 + fi + if ! _contains "$response" '"rrset_type": "TXT"'; then + _debug "Does not have a _acme-challenge TXT record yet." + return 1 + fi + if _contains "$response" '"rrset_values": \[\]'; then + _debug "Empty rrset_values for TXT record, no previous TXT record." + return 1 + fi + _debug "Already has TXT record." + _rrset_values=$(echo "$response" | _egrep_o 'rrset_values.*\[.*\]' | + _egrep_o '\[".*\"]') + return 0 +} + _gandi_livedns_rest() { m=$1 ep="$2" @@ -105,7 +165,12 @@ _gandi_livedns_rest() { _debug "$ep" export _H1="Content-Type: application/json" - export _H2="X-Api-Key: $GANDI_LIVEDNS_KEY" + + if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then + export _H2="Authorization: Bearer $GANDI_LIVEDNS_TOKEN" + else + export _H2="X-Api-Key: $GANDI_LIVEDNS_KEY" + fi if [ "$m" = "GET" ]; then response="$(_get "$GANDI_LIVEDNS_API/$ep")" diff --git a/dnsapi/dns_gcloud.sh b/dnsapi/dns_gcloud.sh index 99fbf410..dc82c09d 100755 --- a/dnsapi/dns_gcloud.sh +++ b/dnsapi/dns_gcloud.sh @@ -39,10 +39,10 @@ dns_gcloud_rm() { _dns_gcloud_start_tr || return $? _dns_gcloud_get_rrdatas || return $? echo "$rrdatas" | _dns_gcloud_remove_rrs || return $? - echo "$rrdatas" | grep -F -v "\"$txtvalue\"" | _dns_gcloud_add_rrs || return $? + echo "$rrdatas" | grep -F -v -- "\"$txtvalue\"" | _dns_gcloud_add_rrs || return $? _dns_gcloud_execute_tr || return $? - _info "$fulldomain record added" + _info "$fulldomain record removed" } #################### Private functions below ################################## @@ -78,8 +78,8 @@ _dns_gcloud_execute_tr() { for i in $(seq 1 120); do if gcloud dns record-sets changes list \ --zone="$managedZone" \ - --filter='status != done' \ - | grep -q '^.*'; then + --filter='status != done' | + grep -q '^.*'; then _info "_dns_gcloud_execute_tr: waiting for transaction to be comitted ($i/120)..." sleep 5 else @@ -93,12 +93,12 @@ _dns_gcloud_execute_tr() { } _dns_gcloud_remove_rrs() { - if ! xargs --no-run-if-empty gcloud dns record-sets transaction remove \ + if ! xargs -r gcloud dns record-sets transaction remove \ --name="$fulldomain." \ --ttl="$ttl" \ --type=TXT \ --zone="$managedZone" \ - --transaction-file="$tr"; then + --transaction-file="$tr" --; then _debug tr "$(cat "$tr")" rm -r "$trd" _err "_dns_gcloud_remove_rrs: failed to remove RRs" @@ -108,12 +108,12 @@ _dns_gcloud_remove_rrs() { _dns_gcloud_add_rrs() { ttl=60 - if ! xargs --no-run-if-empty gcloud dns record-sets transaction add \ + if ! xargs -r gcloud dns record-sets transaction add \ --name="$fulldomain." \ --ttl="$ttl" \ --type=TXT \ --zone="$managedZone" \ - --transaction-file="$tr"; then + --transaction-file="$tr" --; then _debug tr "$(cat "$tr")" rm -r "$trd" _err "_dns_gcloud_add_rrs: failed to add RRs" @@ -131,17 +131,17 @@ _dns_gcloud_find_zone() { filter="$filter$part. " part="$(echo "$part" | sed 's/[^.]*\.*//')" done - filter="$filter)" + filter="$filter) AND visibility=public" _debug filter "$filter" - # List domains and find the longest match (in case of some levels of delegation) + # List domains and find the zone with the deepest sub-domain (in case of some levels of delegation) if ! match=$(gcloud dns managed-zones list \ --format="value(name, dnsName)" \ - --filter="$filter" \ - | while read -r dnsName name; do - printf "%s\t%s\t%s\n" "${#dnsName}" "$dnsName" "$name" - done \ - | sort -n -r | _head_n 1 | cut -f2,3 | grep '^.*'); then + --filter="$filter" | + while read -r dnsName name; do + printf "%s\t%s\t%s\n" "$(echo "$name" | awk -F"." '{print NF-1}')" "$dnsName" "$name" + done | + sort -n -r | _head_n 1 | cut -f2,3 | grep '^.*'); then _err "_dns_gcloud_find_zone: Can't find a matching managed zone! Perhaps wrong project or gcloud credentials?" return 1 fi @@ -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_gcore.sh b/dnsapi/dns_gcore.sh new file mode 100755 index 00000000..5f7f037e --- /dev/null +++ b/dnsapi/dns_gcore.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env sh + +# +#GCORE_Key='773$7b7adaf2a2b32bfb1b83787b4ff32a67eb178e3ada1af733e47b1411f2461f7f4fa7ed7138e2772a46124377bad7384b3bb8d87748f87b3f23db4b8bbe41b2bb' +# + +GCORE_Api="https://api.gcore.com/dns/v2" +GCORE_Doc="https://api.gcore.com/docs/dns" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_gcore_add() { + fulldomain=$1 + txtvalue=$2 + + GCORE_Key="${GCORE_Key:-$(_readaccountconf_mutable GCORE_Key)}" + + if [ -z "$GCORE_Key" ]; then + GCORE_Key="" + _err "You didn't specify a Gcore api key yet." + _err "You can get yours from here $GCORE_Doc" + return 1 + fi + + #save the api key to the account conf file. + _saveaccountconf_mutable GCORE_Key "$GCORE_Key" + + _debug "First detect the zone name" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _zone_name "$_zone_name" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _gcore_rest GET "zones/$_zone_name/$fulldomain/TXT" + payload="" + + if echo "$response" | grep "record is not found" >/dev/null; then + _info "Record doesn't exists" + payload="{\"resource_records\":[{\"content\":[\"$txtvalue\"],\"enabled\":true}],\"ttl\":120}" + elif echo "$response" | grep "$txtvalue" >/dev/null; then + _info "Already exists, OK" + return 0 + elif echo "$response" | tr -d " " | grep \"name\":\""$fulldomain"\",\"type\":\"TXT\" >/dev/null; then + _info "Record with mismatch txtvalue, try update it" + payload=$(echo "$response" | tr -d " " | sed 's/"updated_at":[0-9]\+,//g' | sed 's/"meta":{}}]}/"meta":{}},{"content":['\""$txtvalue"\"'],"enabled":true}]}/') + fi + + # For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so + # we can not use updating anymore. + # count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) + # _debug count "$count" + # if [ "$count" = "0" ]; then + _info "Adding record" + if _gcore_rest PUT "zones/$_zone_name/$fulldomain/TXT" "$payload"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + elif _contains "$response" "rrset is 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 +} + +#fulldomain txtvalue +dns_gcore_rm() { + fulldomain=$1 + txtvalue=$2 + + GCORE_Key="${GCORE_Key:-$(_readaccountconf_mutable GCORE_Key)}" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _zone_name "$_zone_name" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _gcore_rest GET "zones/$_zone_name/$fulldomain/TXT" + + if echo "$response" | grep "record is not found" >/dev/null; then + _info "No such txt recrod" + return 0 + fi + + if ! echo "$response" | tr -d " " | grep \"name\":\""$fulldomain"\",\"type\":\"TXT\" >/dev/null; then + _err "Error: $response" + return 1 + fi + + if ! echo "$response" | tr -d " " | grep \""$txtvalue"\" >/dev/null; then + _info "No such txt recrod" + return 0 + fi + + count="$(echo "$response" | grep -o "content" | wc -l)" + + if [ "$count" = "1" ]; then + if ! _gcore_rest DELETE "zones/$_zone_name/$fulldomain/TXT"; then + _err "Delete record error. $response" + return 1 + fi + return 0 + fi + + payload="$(echo "$response" | tr -d " " | sed 's/"updated_at":[0-9]\+,//g' | sed 's/{"id":[0-9]\+,"content":\["'"$txtvalue"'"\],"enabled":true,"meta":{}}//' | sed 's/\[,/\[/' | sed 's/,,/,/' | sed 's/,\]/\]/')" + if ! _gcore_rest PUT "zones/$_zone_name/$fulldomain/TXT" "$payload"; then + _err "Delete record error. $response" + fi +} + +#################### Private functions below ################################## +#_acme-challenge.sub.domain.com +#returns +# _sub_domain=_acme-challenge.sub or _acme-challenge +# _domain=domain.com +# _zone_name=domain.com or sub.domain.com +_get_root() { + domain=$1 + i=1 + p=1 + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _gcore_rest GET "zones/$h"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\""; then + _zone_name=$h + if [ "$_zone_name" ]; 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 +} + +_gcore_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + key_trimmed=$(echo "$GCORE_Key" | tr -d '"') + + export _H1="Content-Type: application/json" + export _H2="Authorization: APIKey $key_trimmed" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$GCORE_Api/$ep" "" "$m")" + else + response="$(_get "$GCORE_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_gd.sh b/dnsapi/dns_gd.sh index 7cf47386..1729115e 100755 --- a/dnsapi/dns_gd.sh +++ b/dnsapi/dns_gd.sh @@ -1,10 +1,12 @@ #!/usr/bin/env sh #Godaddy domain api +# Get API key and secret from https://developer.godaddy.com/ # -#GD_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" +# GD_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" +# GD_Secret="asdfsdfsfsdfsdfdfsdf" # -#GD_Secret="asdfsdfsfsdfsdfdfsdf" +# Ex.: acme.sh --issue --staging --dns dns_gd -d "*.s.example.com" -d "s.example.com" GD_Api="https://api.godaddy.com/v1" @@ -20,8 +22,8 @@ dns_gd_add() { if [ -z "$GD_Key" ] || [ -z "$GD_Secret" ]; then GD_Key="" GD_Secret="" - _err "You don't specify godaddy api key and secret yet." - _err "Please create you key and try again." + _err "You didn't specify godaddy api key and secret yet." + _err "Please create your key and try again." return 1 fi @@ -44,14 +46,15 @@ dns_gd_add() { fi if _contains "$response" "$txtvalue"; then - _info "The record is existing, skip" + _info "This record already exists, skipping" return 0 fi _add_data="{\"data\":\"$txtvalue\"}" for t in $(echo "$response" | tr '{' "\n" | grep "\"name\":\"$_sub_domain\"" | tr ',' "\n" | grep '"data"' | cut -d : -f 2); do _debug2 t "$t" - if [ "$t" ]; then + # ignore empty (previously removed) records, to prevent useless _acme-challenge TXT entries + if [ "$t" ] && [ "$t" != '""' ]; then _add_data="$_add_data,{\"data\":$t}" fi done @@ -59,13 +62,25 @@ dns_gd_add() { _info "Adding record" if _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[$_add_data]"; then - _info "Added, sleeping 10 seconds" - _sleep 10 - #todo: check if the record takes effect - return 0 + _debug "Checking updated records of '${fulldomain}'" + + if ! _gd_rest GET "domains/$_domain/records/TXT/$_sub_domain"; then + _err "Validating TXT record for '${fulldomain}' with rest error [$?]." "$response" + return 1 + fi + + if ! _contains "$response" "$txtvalue"; then + _err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!" + return 1 + fi + else + _err "Add txt record error, value '${txtvalue}' for '${fulldomain}' was not set." + return 1 fi - _err "Add txt record error." - return 1 + + _sleep 10 + _info "Added TXT record '${txtvalue}' for '${fulldomain}'." + return 0 } #fulldomain @@ -91,7 +106,7 @@ dns_gd_rm() { fi if ! _contains "$response" "$txtvalue"; then - _info "The record is not existing, skip" + _info "The record does not exist, skip" return 0 fi @@ -107,11 +122,20 @@ dns_gd_rm() { fi done if [ -z "$_add_data" ]; then - _add_data="{\"data\":\"\"}" + # delete empty record + _debug "Delete last record for '${fulldomain}'" + if ! _gd_rest DELETE "domains/$_domain/records/TXT/$_sub_domain"; then + _err "Cannot delete empty TXT record for '$fulldomain'" + return 1 + fi + else + # remove specific TXT value, keeping other entries + _debug2 _add_data "$_add_data" + if ! _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[$_add_data]"; then + _err "Cannot update TXT record for '$fulldomain'" + return 1 + fi fi - _debug2 _add_data "$_add_data" - - _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[$_add_data]" } #################### Private functions below ################################## @@ -156,15 +180,15 @@ _gd_rest() { export _H1="Authorization: sso-key $GD_Key:$GD_Secret" export _H2="Content-Type: application/json" - if [ "$data" ]; then - _debug data "$data" + if [ "$data" ] || [ "$m" = "DELETE" ]; then + _debug "data ($m): " "$data" response="$(_post "$data" "$GD_Api/$ep" "" "$m")" else response="$(_get "$GD_Api/$ep")" fi if [ "$?" != "0" ]; then - _err "error $ep" + _err "error on rest call ($m): $ep" return 1 fi _debug2 response "$response" diff --git a/dnsapi/dns_gdnsdk.sh b/dnsapi/dns_gdnsdk.sh deleted file mode 100755 index 7dc7894a..00000000 --- a/dnsapi/dns_gdnsdk.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env sh -#Author: Herman Sletteng -#Report Bugs here: https://github.com/loial/acme.sh -# -# -# Note, gratisdns requires a login first, so the script needs to handle -# temporary cookies. Since acme.sh _get/_post currently don't directly support -# cookies, I've defined wrapper functions _myget/_mypost to set the headers - -GDNSDK_API="https://admin.gratisdns.com" -######## Public functions ##################### -#Usage: dns_gdnsdk_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_gdnsdk_add() { - fulldomain=$1 - txtvalue=$2 - _info "Using gratisdns.dk" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - if ! _gratisdns_login; then - _err "Login failed!" - return 1 - fi - #finding domain zone - if ! _get_domain; then - _err "No matching root domain for $fulldomain found" - return 1 - fi - # adding entry - _info "Adding the entry" - _mypost "action=dns_primary_record_added_txt&user_domain=$_domain&name=$fulldomain&txtdata=$txtvalue&ttl=1" - if _successful_update; then return 0; fi - _err "Couldn't create entry!" - return 1 -} - -#Usage: fulldomain txtvalue -#Remove the txt record after validation. -dns_gdnsdk_rm() { - fulldomain=$1 - txtvalue=$2 - _info "Using gratisdns.dk" - _debug fulldomain "$fulldomain" - _debug txtvalue "$txtvalue" - if ! _gratisdns_login; then - _err "Login failed!" - return 1 - fi - if ! _get_domain; 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..." - _mypost "action=dns_primary_delete_txt&user_domain=$_domain&id=$_id" - # removing entry - - if _successful_update; then return 0; fi - _err "Couldn't delete entry!" - return 1 -} - -#################### Private functions below ################################## - -_checkcredentials() { - GDNSDK_Username="${GDNSDK_Username:-$(_readaccountconf_mutable GDNSDK_Username)}" - GDNSDK_Password="${GDNSDK_Password:-$(_readaccountconf_mutable GDNSDK_Password)}" - - if [ -z "$GDNSDK_Username" ] || [ -z "$GDNSDK_Password" ]; then - GDNSDK_Username="" - GDNSDK_Password="" - _err "You haven't specified gratisdns.dk username and password yet." - _err "Please add credentials and try again." - return 1 - fi - #save the credentials to the account conf file. - _saveaccountconf_mutable GDNSDK_Username "$GDNSDK_Username" - _saveaccountconf_mutable GDNSDK_Password "$GDNSDK_Password" - return 0 -} - -_checkcookie() { - GDNSDK_Cookie="${GDNSDK_Cookie:-$(_readaccountconf_mutable GDNSDK_Cookie)}" - if [ -z "$GDNSDK_Cookie" ]; then - _debug "No cached cookie found" - return 1 - fi - _myget "action=" - if (echo "$_result" | grep -q "logmeout"); then - _debug "Cached cookie still valid" - return 0 - fi - _debug "Cached cookie no longer valid" - GDNSDK_Cookie="" - _saveaccountconf_mutable GDNSDK_Cookie "$GDNSDK_Cookie" - return 1 -} - -_gratisdns_login() { - if ! _checkcredentials; then return 1; fi - - if _checkcookie; then - _debug "Already logged in" - return 0 - fi - _debug "Logging into GratisDNS with user $GDNSDK_Username" - - if ! _mypost "login=$GDNSDK_Username&password=$GDNSDK_Password&action=logmein"; then - _err "GratisDNS login failed for user $GDNSDK_Username bad RC from _post" - return 1 - fi - - GDNSDK_Cookie="$(grep -A 15 '302 Found' "$HTTP_HEADER" | _egrep_o 'Cookie: [^;]*' | _head_n 1 | cut -d ' ' -f2)" - - if [ -z "$GDNSDK_Cookie" ]; then - _err "GratisDNS login failed for user $GDNSDK_Username. Check $HTTP_HEADER file" - return 1 - fi - export GDNSDK_Cookie - _saveaccountconf_mutable GDNSDK_Cookie "$GDNSDK_Cookie" - return 0 -} - -_myget() { - #Adds cookie to request - export _H1="Cookie: $GDNSDK_Cookie" - _result=$(_get "$GDNSDK_API?$1") -} -_mypost() { - #Adds cookie to request - export _H1="Cookie: $GDNSDK_Cookie" - _result=$(_post "$1" "$GDNSDK_API") -} - -_get_domain() { - _myget 'action=dns_primarydns' - _domains=$(echo "$_result" | _egrep_o ' domain="[[:alnum:].-_]+' | sed 's/^.*"//') - if [ -z "$_domains" ]; then - _err "Primary domain list not found!" - return 1 - fi - for _domain in $_domains; do - if (_endswith "$fulldomain" "$_domain"); then - _debug "Root domain: $_domain" - return 0 - fi - done - return 1 -} - -_successful_update() { - if (echo "$_result" | grep -q 'table-success'); then return 0; fi - return 1 -} - -_findentry() { - #returns id of dns entry, if it exists - _myget "action=dns_primary_changeDNSsetup&user_domain=$_domain" - _id=$(echo "$_result" | _egrep_o "$1\s*$2[^?]*[^&]*&id=[^&]*" | sed 's/^.*=//') - if [ -n "$_id" ]; then - _debug "Entry found with _id=$_id" - return 0 - fi - return 1 -} diff --git a/dnsapi/dns_geoscaling.sh b/dnsapi/dns_geoscaling.sh new file mode 100755 index 00000000..6ccf4daf --- /dev/null +++ b/dnsapi/dns_geoscaling.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env sh + +######################################################################## +# Geoscaling hook script for acme.sh +# +# Environment variables: +# +# - $GEOSCALING_Username (your Geoscaling username - this is usually NOT an amail address) +# - $GEOSCALING_Password (your Geoscaling password) + +#-- dns_geoscaling_add() - Add TXT record -------------------------------------- +# Usage: dns_geoscaling_add _acme-challenge.subdomain.domain.com "XyZ123..." + +dns_geoscaling_add() { + full_domain=$1 + txt_value=$2 + _info "Using DNS-01 Geoscaling DNS2 hook" + + GEOSCALING_Username="${GEOSCALING_Username:-$(_readaccountconf_mutable GEOSCALING_Username)}" + GEOSCALING_Password="${GEOSCALING_Password:-$(_readaccountconf_mutable GEOSCALING_Password)}" + if [ -z "$GEOSCALING_Username" ] || [ -z "$GEOSCALING_Password" ]; then + GEOSCALING_Username= + GEOSCALING_Password= + _err "No auth details provided. Please set user credentials using the \$GEOSCALING_Username and \$GEOSCALING_Password environment variables." + return 1 + fi + _saveaccountconf_mutable GEOSCALING_Username "${GEOSCALING_Username}" + _saveaccountconf_mutable GEOSCALING_Password "${GEOSCALING_Password}" + + # Fills in the $zone_id and $zone_name + find_zone "${full_domain}" || return 1 + _debug "Zone id '${zone_id}' will be used." + + # We're logged in here + + # we should add ${full_domain} minus the trailing ${zone_name} + + prefix=$(echo "${full_domain}" | sed "s|\\.${zone_name}\$||") + + body="id=${zone_id}&name=${prefix}&type=TXT&content=${txt_value}&ttl=300&prio=0" + + do_post "$body" "https://www.geoscaling.com/dns2/ajax/add_record.php" + exit_code="$?" + if [ "${exit_code}" -eq 0 ]; then + _info "TXT record added successfully." + else + _err "Couldn't add the TXT record." + fi + do_logout + return "${exit_code}" +} + +#-- dns_geoscaling_rm() - Remove TXT record ------------------------------------ +# Usage: dns_geoscaling_rm _acme-challenge.subdomain.domain.com "XyZ123..." + +dns_geoscaling_rm() { + full_domain=$1 + txt_value=$2 + _info "Cleaning up after DNS-01 Geoscaling DNS2 hook" + + GEOSCALING_Username="${GEOSCALING_Username:-$(_readaccountconf_mutable GEOSCALING_Username)}" + GEOSCALING_Password="${GEOSCALING_Password:-$(_readaccountconf_mutable GEOSCALING_Password)}" + if [ -z "$GEOSCALING_Username" ] || [ -z "$GEOSCALING_Password" ]; then + GEOSCALING_Username= + GEOSCALING_Password= + _err "No auth details provided. Please set user credentials using the \$GEOSCALING_Username and \$GEOSCALING_Password environment variables." + return 1 + fi + _saveaccountconf_mutable GEOSCALING_Username "${GEOSCALING_Username}" + _saveaccountconf_mutable GEOSCALING_Password "${GEOSCALING_Password}" + + # fills in the $zone_id + find_zone "${full_domain}" || return 1 + _debug "Zone id '${zone_id}' will be used." + + # Here we're logged in + # Find the record id to clean + + # get the domain + response=$(do_get "https://www.geoscaling.com/dns2/index.php?module=domain&id=${zone_id}") + _debug2 "response" "$response" + + table="$(echo "${response}" | tr -d '\n' | sed 's|.*
Basic Records
.*||')" + _debug2 table "${table}" + names=$(echo "${table}" | _egrep_o 'id="[0-9]+\.name">[^<]*' | sed 's|||; s|.*>||') + ids=$(echo "${table}" | _egrep_o 'id="[0-9]+\.name">[^<]*' | sed 's|\.name">.*||; s|id="||') + types=$(echo "${table}" | _egrep_o 'id="[0-9]+\.type">[^<]*' | sed 's|||; s|.*>||') + values=$(echo "${table}" | _egrep_o 'id="[0-9]+\.content">[^<]*' | sed 's|||; s|.*>||') + + _debug2 names "${names}" + _debug2 ids "${ids}" + _debug2 types "${types}" + _debug2 values "${values}" + + # look for line whose name is ${full_domain}, whose type is TXT, and whose value is ${txt_value} + line_num="$(echo "${values}" | grep -F -n -- "${txt_value}" | _head_n 1 | cut -d ':' -f 1)" + _debug2 line_num "${line_num}" + found_id= + if [ -n "$line_num" ]; then + type=$(echo "${types}" | sed -n "${line_num}p") + name=$(echo "${names}" | sed -n "${line_num}p") + id=$(echo "${ids}" | sed -n "${line_num}p") + + _debug2 type "$type" + _debug2 name "$name" + _debug2 id "$id" + _debug2 full_domain "$full_domain" + + if [ "${type}" = "TXT" ] && [ "${name}" = "${full_domain}" ]; then + found_id=${id} + fi + fi + + if [ "${found_id}" = "" ]; then + _err "Can not find record id." + return 0 + fi + + # Remove the record + body="id=${zone_id}&record_id=${found_id}" + response=$(do_post "$body" "https://www.geoscaling.com/dns2/ajax/delete_record.php") + exit_code="$?" + if [ "$exit_code" -eq 0 ]; then + _info "Record removed successfully." + else + _err "Could not clean (remove) up the record. Please go to Geoscaling administration interface and clean it by hand." + fi + do_logout + return "${exit_code}" +} + +########################## PRIVATE FUNCTIONS ########################### + +do_get() { + _url=$1 + export _H1="Cookie: $geoscaling_phpsessid_cookie" + _get "${_url}" +} + +do_post() { + _body=$1 + _url=$2 + export _H1="Cookie: $geoscaling_phpsessid_cookie" + _post "${_body}" "${_url}" +} + +do_login() { + + _info "Logging in..." + + username_encoded="$(printf "%s" "${GEOSCALING_Username}" | _url_encode)" + password_encoded="$(printf "%s" "${GEOSCALING_Password}" | _url_encode)" + body="username=${username_encoded}&password=${password_encoded}" + + response=$(_post "$body" "https://www.geoscaling.com/dns2/index.php?module=auth") + _debug2 response "${response}" + + #retcode=$(grep '^HTTP[^ ]*' "${HTTP_HEADER}" | _head_n 1 | _egrep_o '[0-9]+$') + retcode=$(grep '^HTTP[^ ]*' "${HTTP_HEADER}" | _head_n 1 | cut -d ' ' -f 2) + + if [ "$retcode" != "302" ]; then + _err "Geoscaling login failed for user ${GEOSCALING_Username}. Check ${HTTP_HEADER} file" + return 1 + fi + + geoscaling_phpsessid_cookie="$(grep -i '^set-cookie:' "${HTTP_HEADER}" | _egrep_o 'PHPSESSID=[^;]*;' | tr -d ';')" + return 0 + +} + +do_logout() { + _info "Logging out." + response="$(do_get "https://www.geoscaling.com/dns2/index.php?module=auth")" + _debug2 response "$response" + return 0 +} + +find_zone() { + domain="$1" + + # do login + do_login || return 1 + + # get zones + response="$(do_get "https://www.geoscaling.com/dns2/index.php?module=domains")" + + table="$(echo "${response}" | tr -d '\n' | sed 's|.*
Your domains
.*||')" + _debug2 table "${table}" + zone_names="$(echo "${table}" | _egrep_o '[^<]*' | sed 's|||;s|||')" + _debug2 _matches "${zone_names}" + # Zone names and zone IDs are in same order + zone_ids=$(echo "${table}" | _egrep_o '' | sed 's|.*id=||;s|. .*||') + + _debug2 "These are the zones on this Geoscaling account:" + _debug2 "zone_names" "${zone_names}" + _debug2 "And these are their respective IDs:" + _debug2 "zone_ids" "${zone_ids}" + if [ -z "${zone_names}" ] || [ -z "${zone_ids}" ]; then + _err "Can not get zone names or IDs." + return 1 + fi + # Walk through all possible zone names + strip_counter=1 + while true; do + attempted_zone=$(echo "${domain}" | cut -d . -f ${strip_counter}-) + + # All possible zone names have been tried + if [ -z "${attempted_zone}" ]; then + _err "No zone for domain '${domain}' found." + return 1 + fi + + _debug "Looking for zone '${attempted_zone}'" + + line_num="$(echo "${zone_names}" | grep -n "^${attempted_zone}\$" | _head_n 1 | cut -d : -f 1)" + _debug2 line_num "${line_num}" + if [ "$line_num" ]; then + zone_id=$(echo "${zone_ids}" | sed -n "${line_num}p") + zone_name=$(echo "${zone_names}" | sed -n "${line_num}p") + if [ -z "${zone_id}" ]; then + _err "Can not find zone id." + return 1 + fi + _debug "Found relevant zone '${attempted_zone}' with id '${zone_id}' - will be used for domain '${domain}'." + return 0 + fi + + _debug "Zone '${attempted_zone}' doesn't exist, let's try a less specific zone." + strip_counter=$(_math "${strip_counter}" + 1) + done +} +# vim: et:ts=2:sw=2: diff --git a/dnsapi/dns_googledomains.sh b/dnsapi/dns_googledomains.sh new file mode 100755 index 00000000..63e3073b --- /dev/null +++ b/dnsapi/dns_googledomains.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env sh + +# Author: Alex Leigh +# Created: 2023-03-02 + +#GOOGLEDOMAINS_ACCESS_TOKEN="xxxx" +#GOOGLEDOMAINS_ZONE="xxxx" +GOOGLEDOMAINS_API="https://acmedns.googleapis.com/v1/acmeChallengeSets" + +######## Public functions ######## + +#Usage: dns_googledomains_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_googledomains_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Invoking Google Domains ACME DNS API." + + if ! _dns_googledomains_setup; then + return 1 + fi + + zone="$(_dns_googledomains_get_zone "$fulldomain")" + if [ -z "$zone" ]; then + _err "Could not find a Google Domains-managed zone containing the requested domain." + return 1 + fi + + _debug zone "$zone" + _debug txtvalue "$txtvalue" + + _info "Adding TXT record for $fulldomain." + if _dns_googledomains_api "$zone" ":rotateChallenges" "{\"accessToken\":\"$GOOGLEDOMAINS_ACCESS_TOKEN\",\"recordsToAdd\":[{\"fqdn\":\"$fulldomain\",\"digest\":\"$txtvalue\"}],\"keepExpiredRecords\":true}"; then + if _contains "$response" "$txtvalue"; then + _info "TXT record added." + return 0 + else + _err "Error adding TXT record." + return 1 + fi + fi + + _err "Error adding TXT record." + return 1 +} + +#Usage: dns_googledomains_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_googledomains_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Invoking Google Domains ACME DNS API." + + if ! _dns_googledomains_setup; then + return 1 + fi + + zone="$(_dns_googledomains_get_zone "$fulldomain")" + if [ -z "$zone" ]; then + _err "Could not find a Google Domains-managed domain based on request." + return 1 + fi + + _debug zone "$zone" + _debug txtvalue "$txtvalue" + + _info "Removing TXT record for $fulldomain." + if _dns_googledomains_api "$zone" ":rotateChallenges" "{\"accessToken\":\"$GOOGLEDOMAINS_ACCESS_TOKEN\",\"recordsToRemove\":[{\"fqdn\":\"$fulldomain\",\"digest\":\"$txtvalue\"}],\"keepExpiredRecords\":true}"; then + if _contains "$response" "$txtvalue"; then + _err "Error removing TXT record." + return 1 + else + _info "TXT record removed." + return 0 + fi + fi + + _err "Error removing TXT record." + return 1 +} + +######## Private functions ######## + +_dns_googledomains_setup() { + if [ -n "$GOOGLEDOMAINS_SETUP_COMPLETED" ]; then + return 0 + fi + + GOOGLEDOMAINS_ACCESS_TOKEN="${GOOGLEDOMAINS_ACCESS_TOKEN:-$(_readaccountconf_mutable GOOGLEDOMAINS_ACCESS_TOKEN)}" + GOOGLEDOMAINS_ZONE="${GOOGLEDOMAINS_ZONE:-$(_readaccountconf_mutable GOOGLEDOMAINS_ZONE)}" + + if [ -z "$GOOGLEDOMAINS_ACCESS_TOKEN" ]; then + GOOGLEDOMAINS_ACCESS_TOKEN="" + _err "Google Domains access token was not specified." + _err "Please visit Google Domains Security settings to provision an ACME DNS API access token." + return 1 + fi + + if [ "$GOOGLEDOMAINS_ZONE" ]; then + _savedomainconf GOOGLEDOMAINS_ACCESS_TOKEN "$GOOGLEDOMAINS_ACCESS_TOKEN" + _savedomainconf GOOGLEDOMAINS_ZONE "$GOOGLEDOMAINS_ZONE" + else + _saveaccountconf_mutable GOOGLEDOMAINS_ACCESS_TOKEN "$GOOGLEDOMAINS_ACCESS_TOKEN" + _clearaccountconf_mutable GOOGLEDOMAINS_ZONE + _clearaccountconf GOOGLEDOMAINS_ZONE + fi + + _debug GOOGLEDOMAINS_ACCESS_TOKEN "$GOOGLEDOMAINS_ACCESS_TOKEN" + _debug GOOGLEDOMAINS_ZONE "$GOOGLEDOMAINS_ZONE" + + GOOGLEDOMAINS_SETUP_COMPLETED=1 + return 0 +} + +_dns_googledomains_get_zone() { + domain=$1 + + # Use zone directly if provided + if [ "$GOOGLEDOMAINS_ZONE" ]; then + if ! _dns_googledomains_api "$GOOGLEDOMAINS_ZONE"; then + return 1 + fi + + echo "$GOOGLEDOMAINS_ZONE" + return 0 + fi + + i=2 + while true; do + curr=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug curr "$curr" + + if [ -z "$curr" ]; then + return 1 + fi + + if _dns_googledomains_api "$curr"; then + echo "$curr" + return 0 + fi + + i=$(_math "$i" + 1) + done + + return 1 +} + +_dns_googledomains_api() { + zone=$1 + apimethod=$2 + data="$3" + + if [ -z "$data" ]; then + response="$(_get "$GOOGLEDOMAINS_API/$zone$apimethod")" + else + _debug data "$data" + export _H1="Content-Type: application/json" + response="$(_post "$data" "$GOOGLEDOMAINS_API/$zone$apimethod")" + fi + + _debug response "$response" + + if [ "$?" != "0" ]; then + _err "Error" + return 1 + fi + + if _contains "$response" "\"error\": {"; then + return 1 + fi + + return 0 +} diff --git a/dnsapi/dns_he.sh b/dnsapi/dns_he.sh index df00c746..bf4a5030 100755 --- a/dnsapi/dns_he.sh +++ b/dnsapi/dns_he.sh @@ -24,7 +24,7 @@ dns_he_add() { if [ -z "$HE_Username" ] || [ -z "$HE_Password" ]; then HE_Username= HE_Password= - _err "No auth details provided. Please set user credentials using the \$HE_Username and \$HE_Password envoronment variables." + _err "No auth details provided. Please set user credentials using the \$HE_Username and \$HE_Password environment variables." return 1 fi _saveaccountconf_mutable HE_Username "$HE_Username" @@ -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/Successfully removed record.
' \ + _post "$body" "https://dns.he.net/" | + grep '
Successfully removed record.
' \ >/dev/null exit_code="$?" if [ "$exit_code" -eq 0 ]; then @@ -134,9 +134,9 @@ _find_zone() { _zone_ids=$(echo "$_matches" | _egrep_o "hosted_dns_zoneid=[0-9]*&" | cut -d = -f 2 | tr -d '&') _zone_names=$(echo "$_matches" | _egrep_o "name=.*onclick" | cut -d '"' -f 2) _debug2 "These are the zones on this HE account:" - _debug2 "$_zone_names" + _debug2 "_zone_names" "$_zone_names" _debug2 "And these are their respective IDs:" - _debug2 "$_zone_ids" + _debug2 "_zone_ids" "$_zone_ids" if [ -z "$_zone_names" ] || [ -z "$_zone_ids" ]; then _err "Can not get zone names." return 1 @@ -154,10 +154,14 @@ _find_zone() { _debug "Looking for zone \"${_attempted_zone}\"" - line_num="$(echo "$_zone_names" | grep -n "^$_attempted_zone" | cut -d : -f 1)" - + line_num="$(echo "$_zone_names" | grep -n "^$_attempted_zone\$" | _head_n 1 | cut -d : -f 1)" + _debug2 line_num "$line_num" if [ "$line_num" ]; then _zone_id=$(echo "$_zone_ids" | sed -n "${line_num}p") + if [ -z "$_zone_id" ]; then + _err "Can not find zone id." + return 1 + fi _debug "Found relevant zone \"$_attempted_zone\" with id \"$_zone_id\" - will be used for domain \"$_domain\"." return 0 fi diff --git a/dnsapi/dns_hetzner.sh b/dnsapi/dns_hetzner.sh new file mode 100644 index 00000000..911d4a35 --- /dev/null +++ b/dnsapi/dns_hetzner.sh @@ -0,0 +1,252 @@ +#!/usr/bin/env sh + +# +#HETZNER_Token="sdfsdfsdfljlbjkljlkjsdfoiwje" +# + +HETZNER_Api="https://dns.hetzner.com/api/v1" + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +# Ref: https://dns.hetzner.com/api-docs/ +dns_hetzner_add() { + full_domain=$1 + txt_value=$2 + + HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}" + + if [ -z "$HETZNER_Token" ]; then + HETZNER_Token="" + _err "You didn't specify a Hetzner api token." + _err "You can get yours from here https://dns.hetzner.com/settings/api-token." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable HETZNER_Token "$HETZNER_Token" + + _debug "First detect the root zone" + + if ! _get_root "$full_domain"; then + _err "Invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting TXT records" + if ! _find_record "$_sub_domain" "$txt_value"; then + return 1 + fi + + if [ -z "$_record_id" ]; then + _info "Adding record" + if _hetzner_rest POST "records" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then + if _contains "$response" "$txt_value"; then + _info "Record added, OK" + _sleep 2 + return 0 + fi + fi + _err "Add txt record error${_response_error}" + return 1 + else + _info "Found record id: $_record_id." + _info "Record found, do nothing." + return 0 + # we could modify a record, if the names for txt records for *.example.com and example.com would be not the same + #if _hetzner_rest PUT "records/${_record_id}" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$full_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then + # if _contains "$response" "$txt_value"; then + # _info "Modified, OK" + # return 0 + # fi + #fi + #_err "Add txt record error (modify)." + #return 1 + fi +} + +# Usage: full_domain txt_value +# Used to remove the txt record after validation +dns_hetzner_rm() { + full_domain=$1 + txt_value=$2 + + HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}" + + _debug "First detect the root zone" + if ! _get_root "$full_domain"; then + _err "Invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting TXT records" + if ! _find_record "$_sub_domain" "$txt_value"; then + return 1 + fi + + if [ -z "$_record_id" ]; then + _info "Remove not needed. Record not found." + else + if ! _hetzner_rest DELETE "records/$_record_id"; then + _err "Delete record error${_response_error}" + return 1 + fi + _sleep 2 + _info "Record deleted" + fi +} + +#################### Private functions below ################################## +#returns +# _record_id=a8d58f22d6931bf830eaa0ec6464bf81 if found; or 1 if error +_find_record() { + unset _record_id + _record_name=$1 + _record_value=$2 + + if [ -z "$_record_value" ]; then + _record_value='[^"]*' + fi + + _debug "Getting all records" + _hetzner_rest GET "records?zone_id=${_domain_id}" + + if _response_has_error; then + _err "Error${_response_error}" + return 1 + else + _record_id=$( + echo "$response" | + grep -o "{[^\{\}]*\"name\":\"$_record_name\"[^\}]*}" | + grep "\"value\":\"$_record_value\"" | + while read -r record; do + # test for type and + if [ -n "$(echo "$record" | _egrep_o '"type":"TXT"')" ]; then + echo "$record" | _egrep_o '"id":"[^"]*"' | cut -d : -f 2 | tr -d \" + break + fi + done + ) + fi +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + i=1 + p=1 + + domain_without_acme=$(echo "$domain" | cut -d . -f 2-) + domain_param_name=$(echo "HETZNER_Zone_ID_for_${domain_without_acme}" | sed 's/[\.\-]/_/g') + + _debug "Reading zone_id for '$domain_without_acme' from config..." + HETZNER_Zone_ID=$(_readdomainconf "$domain_param_name") + if [ "$HETZNER_Zone_ID" ]; then + _debug "Found, using: $HETZNER_Zone_ID" + if ! _hetzner_rest GET "zones/${HETZNER_Zone_ID}"; then + _debug "Zone with id '$HETZNER_Zone_ID' does not exist." + _cleardomainconf "$domain_param_name" + unset HETZNER_Zone_ID + else + if _contains "$response" "\"id\":\"$HETZNER_Zone_ID\""; then + _domain=$(printf "%s\n" "$response" | _egrep_o '"name":"[^"]*"' | cut -d : -f 2 | tr -d \" | head -n 1) + if [ "$_domain" ]; then + _cut_length=$((${#domain} - ${#_domain} - 1)) + _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cut_length") + _domain_id="$HETZNER_Zone_ID" + return 0 + else + return 1 + fi + else + return 1 + fi + fi + fi + + _debug "Trying to get zone id by domain name for '$domain_without_acme'." + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + _debug h "$h" + + _hetzner_rest GET "zones?name=$h" + + if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then + _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + HETZNER_Zone_ID=$_domain_id + _savedomainconf "$domain_param_name" "$HETZNER_Zone_ID" + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +#returns +# _response_error +_response_has_error() { + unset _response_error + + err_part="$(echo "$response" | _egrep_o '"error":{[^}]*}')" + + if [ -n "$err_part" ]; then + err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2) + err_message=$(echo "$err_part" | _egrep_o '"message":"[^"]+"' | cut -d : -f 2 | tr -d \") + + if [ -n "$err_code" ] && [ -n "$err_message" ]; then + _response_error=" - message: ${err_message}, code: ${err_code}" + return 0 + fi + fi + + return 1 +} + +#returns +# response +_hetzner_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + key_trimmed=$(echo "$HETZNER_Token" | tr -d \") + + export _H1="Content-TType: application/json" + export _H2="Auth-API-Token: $key_trimmed" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$HETZNER_Api/$ep" "" "$m")" + else + response="$(_get "$HETZNER_Api/$ep")" + fi + + if [ "$?" != "0" ] || _response_has_error; then + _debug "Error$_response_error" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_hexonet.sh b/dnsapi/dns_hexonet.sh new file mode 100755 index 00000000..525efe73 --- /dev/null +++ b/dnsapi/dns_hexonet.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env sh + +# +# Hexonet_Login="username!roleId" +# +# Hexonet_Password="rolePassword" + +Hexonet_Api="https://coreapi.1api.net/api/call.cgi" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_hexonet_add() { + fulldomain=$1 + txtvalue=$2 + + Hexonet_Login="${Hexonet_Login:-$(_readaccountconf_mutable Hexonet_Login)}" + Hexonet_Password="${Hexonet_Password:-$(_readaccountconf_mutable Hexonet_Password)}" + if [ -z "$Hexonet_Login" ] || [ -z "$Hexonet_Password" ]; then + Hexonet_Login="" + Hexonet_Password="" + _err "You must export variables: Hexonet_Login and Hexonet_Password" + return 1 + fi + + if ! _contains "$Hexonet_Login" "!"; then + _err "It seems that the Hexonet_Login=$Hexonet_Login is not a restrivteed user." + _err "Please check and retry." + return 1 + fi + + #save the username and password to the account conf file. + _saveaccountconf_mutable Hexonet_Login "$Hexonet_Login" + _saveaccountconf_mutable Hexonet_Password "$Hexonet_Password" + + _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" + _hexonet_rest "command=QueryDNSZoneRRList&dnszone=${h}.&RRTYPE=TXT" + + if ! _contains "$response" "CODE=200"; then + _err "Error" + return 1 + fi + + _info "Adding record" + if _hexonet_rest "command=UpdateDNSZone&dnszone=${_domain}.&addrr0=${_sub_domain}%20IN%20TXT%20${txtvalue}"; then + if _contains "$response" "CODE=200"; then + _info "Added, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + return 1 + +} + +#fulldomain txtvalue +dns_hexonet_rm() { + fulldomain=$1 + txtvalue=$2 + + Hexonet_Login="${Hexonet_Login:-$(_readaccountconf_mutable Hexonet_Login)}" + Hexonet_Password="${Hexonet_Password:-$(_readaccountconf_mutable Hexonet_Password)}" + if [ -z "$Hexonet_Login" ] || [ -z "$Hexonet_Password" ]; then + Hexonet_Login="" + Hexonet_Password="" + _err "You must export variables: Hexonet_Login and Hexonet_Password" + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _hexonet_rest "command=QueryDNSZoneRRList&dnszone=${h}.&RRTYPE=TXT&RR=${_sub_domain}%20IN%20TXT%20\"${txtvalue}\"" + + if ! _contains "$response" "CODE=200"; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "PROPERTY[TOTAL][0]=" | cut -d = -f 2) + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + if ! _hexonet_rest "command=UpdateDNSZone&dnszone=${_domain}.&delrr0=${_sub_domain}%20IN%20TXT%20\"${txtvalue}\""; then + _err "Delete record error." + return 1 + fi + _contains "$response" "CODE=200" + fi + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _hexonet_rest "command=QueryDNSZoneRRList&dnszone=${h}."; then + return 1 + fi + + if _contains "$response" "CODE=200"; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_hexonet_rest() { + query_params="$1" + _debug "$query_params" + + response="$(_get "${Hexonet_Api}?s_login=${Hexonet_Login}&s_pw=${Hexonet_Password}&${query_params}")" + + if [ "$?" != "0" ]; then + _err "error $query_params" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_hostingde.sh b/dnsapi/dns_hostingde.sh new file mode 100644 index 00000000..9e3e5664 --- /dev/null +++ b/dnsapi/dns_hostingde.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env sh + +# hosting.de API + +# Values to export: +# export HOSTINGDE_ENDPOINT='https://secure.hosting.de' +# export HOSTINGDE_APIKEY='xxxxx' + +######## Public functions ##################### + +dns_hostingde_add() { + fulldomain="${1}" + txtvalue="${2}" + _debug "Calling: _hostingde_addRecord() '${fulldomain}' '${txtvalue}'" + _hostingde_apiKey && _hostingde_getZoneConfig && _hostingde_addRecord + return $? +} + +dns_hostingde_rm() { + fulldomain="${1}" + txtvalue="${2}" + _debug "Calling: _hostingde_removeRecord() '${fulldomain}' '${txtvalue}'" + _hostingde_apiKey && _hostingde_getZoneConfig && _hostingde_removeRecord + return $? +} + +#################### own Private functions below ################################## + +_hostingde_apiKey() { + HOSTINGDE_APIKEY="${HOSTINGDE_APIKEY:-$(_readaccountconf_mutable HOSTINGDE_APIKEY)}" + HOSTINGDE_ENDPOINT="${HOSTINGDE_ENDPOINT:-$(_readaccountconf_mutable HOSTINGDE_ENDPOINT)}" + if [ -z "$HOSTINGDE_APIKEY" ] || [ -z "$HOSTINGDE_ENDPOINT" ]; then + HOSTINGDE_APIKEY="" + HOSTINGDE_ENDPOINT="" + _err "You haven't specified hosting.de API key or endpoint yet." + _err "Please create your key and try again." + return 1 + fi + + _saveaccountconf_mutable HOSTINGDE_APIKEY "$HOSTINGDE_APIKEY" + _saveaccountconf_mutable HOSTINGDE_ENDPOINT "$HOSTINGDE_ENDPOINT" +} + +_hostingde_parse() { + find="${1}" + if [ "${2}" ]; then + notfind="${2}" + fi + if [ "${notfind}" ]; then + _egrep_o \""${find}\":.*" | grep -v "${notfind}" | cut -d ':' -f 2 | cut -d ',' -f 1 | tr -d ' ' + else + _egrep_o \""${find}\":.*" | cut -d ':' -f 2 | cut -d ',' -f 1 | tr -d ' ' + fi +} + +_hostingde_getZoneConfig() { + _info "Getting ZoneConfig" + curZone="${fulldomain#*.}" + returnCode=1 + while _contains "${curZone}" "\\."; do + curData="{\"filter\":{\"field\":\"zoneName\",\"value\":\"${curZone}\"},\"limit\":1,\"authToken\":\"${HOSTINGDE_APIKEY}\"}" + curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneConfigsFind")" + _debug "Calling zoneConfigsFind: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneConfigsFind'" + _debug "Result of zoneConfigsFind: '$curResult'" + if _contains "${curResult}" '"status": "error"'; then + if _contains "${curResult}" '"code": 10109'; then + _err "The API-Key is invalid or could not be found" + else + _err "UNKNOWN API ERROR" + fi + returnCode=1 + break + fi + if _contains "${curResult}" '"totalEntries": 1'; then + _info "Retrieved zone data." + _debug "Zone data: '${curResult}'" + zoneConfigId=$(echo "${curResult}" | _hostingde_parse "id") + zoneConfigName=$(echo "${curResult}" | _hostingde_parse "name") + zoneConfigType=$(echo "${curResult}" | _hostingde_parse "type" "FindZoneConfigsResult") + zoneConfigExpire=$(echo "${curResult}" | _hostingde_parse "expire") + zoneConfigNegativeTtl=$(echo "${curResult}" | _hostingde_parse "negativeTtl") + zoneConfigRefresh=$(echo "${curResult}" | _hostingde_parse "refresh") + zoneConfigRetry=$(echo "${curResult}" | _hostingde_parse "retry") + zoneConfigTtl=$(echo "${curResult}" | _hostingde_parse "ttl") + zoneConfigDnsServerGroupId=$(echo "${curResult}" | _hostingde_parse "dnsServerGroupId") + zoneConfigEmailAddress=$(echo "${curResult}" | _hostingde_parse "emailAddress") + zoneConfigDnsSecMode=$(echo "${curResult}" | _hostingde_parse "dnsSecMode") + zoneConfigTemplateValues=$(echo "${curResult}" | _hostingde_parse "templateValues") + + if [ "$zoneConfigTemplateValues" != "null" ]; then + _debug "Zone is tied to a template." + zoneConfigTemplateValuesTemplateId=$(echo "${curResult}" | _hostingde_parse "templateId") + zoneConfigTemplateValuesTemplateName=$(echo "${curResult}" | _hostingde_parse "templateName") + zoneConfigTemplateValuesTemplateReplacementsIPv4=$(echo "${curResult}" | _hostingde_parse "ipv4Replacement") + zoneConfigTemplateValuesTemplateReplacementsIPv6=$(echo "${curResult}" | _hostingde_parse "ipv6Replacement") + zoneConfigTemplateValuesTemplateReplacementsMailIPv4=$(echo "${curResult}" | _hostingde_parse "mailIpv4Replacement") + zoneConfigTemplateValuesTemplateReplacementsMailIPv6=$(echo "${curResult}" | _hostingde_parse "mailIpv6Replacement") + zoneConfigTemplateValuesTemplateTieToTemplate=$(echo "${curResult}" | _hostingde_parse "tieToTemplate") + + zoneConfigTemplateValues="{\"templateId\":${zoneConfigTemplateValuesTemplateId},\"templateName\":${zoneConfigTemplateValuesTemplateName},\"templateReplacements\":{\"ipv4Replacement\":${zoneConfigTemplateValuesTemplateReplacementsIPv4},\"ipv6Replacement\":${zoneConfigTemplateValuesTemplateReplacementsIPv6},\"mailIpv4Replacement\":${zoneConfigTemplateValuesTemplateReplacementsMailIPv4},\"mailIpv6Replacement\":${zoneConfigTemplateValuesTemplateReplacementsMailIPv6}},\"tieToTemplate\":${zoneConfigTemplateValuesTemplateTieToTemplate}}" + _debug "Template values: '{$zoneConfigTemplateValues}'" + fi + + if [ "${zoneConfigType}" != "\"NATIVE\"" ]; then + _err "Zone is not native" + returnCode=1 + break + fi + _debug "zoneConfigId '${zoneConfigId}'" + returnCode=0 + break + fi + curZone="${curZone#*.}" + done + if [ $returnCode -ne 0 ]; then + _info "ZoneEnd reached, Zone ${curZone} not found in hosting.de API" + fi + return $returnCode +} + +_hostingde_getZoneStatus() { + _debug "Checking Zone status" + curData="{\"filter\":{\"field\":\"zoneConfigId\",\"value\":${zoneConfigId}},\"limit\":1,\"authToken\":\"${HOSTINGDE_APIKEY}\"}" + curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zonesFind")" + _debug "Calling zonesFind '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zonesFind'" + _debug "Result of zonesFind '$curResult'" + zoneStatus=$(echo "${curResult}" | _hostingde_parse "status" "success") + _debug "zoneStatus '${zoneStatus}'" + return 0 +} + +_hostingde_addRecord() { + _info "Adding record to zone" + _hostingde_getZoneStatus + _debug "Result of zoneStatus: '${zoneStatus}'" + while [ "${zoneStatus}" != "\"active\"" ]; do + _sleep 5 + _hostingde_getZoneStatus + _debug "Result of zoneStatus: '${zoneStatus}'" + done + curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":${zoneConfigId},\"name\":${zoneConfigName},\"type\":${zoneConfigType},\"dnsServerGroupId\":${zoneConfigDnsServerGroupId},\"dnsSecMode\":${zoneConfigDnsSecMode},\"emailAddress\":${zoneConfigEmailAddress},\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}},\"templateValues\":${zoneConfigTemplateValues}},\"recordsToAdd\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\",\"ttl\":3600}]}" + curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")" + _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'" + _debug "Result of zoneUpdate: '$curResult'" + if _contains "${curResult}" '"status": "error"'; then + if _contains "${curResult}" '"code": 10109'; then + _err "The API-Key is invalid or could not be found" + else + _err "UNKNOWN API ERROR" + fi + return 1 + fi + return 0 +} + +_hostingde_removeRecord() { + _info "Removing record from zone" + _hostingde_getZoneStatus + _debug "Result of zoneStatus: '$zoneStatus'" + while [ "$zoneStatus" != "\"active\"" ]; do + _sleep 5 + _hostingde_getZoneStatus + _debug "Result of zoneStatus: '$zoneStatus'" + done + curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":${zoneConfigId},\"name\":${zoneConfigName},\"type\":${zoneConfigType},\"dnsServerGroupId\":${zoneConfigDnsServerGroupId},\"dnsSecMode\":${zoneConfigDnsSecMode},\"emailAddress\":${zoneConfigEmailAddress},\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}},\"templateValues\":${zoneConfigTemplateValues}},\"recordsToDelete\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\"}]}" + curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")" + _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'" + _debug "Result of zoneUpdate: '$curResult'" + if _contains "${curResult}" '"status": "error"'; then + if _contains "${curResult}" '"code": 10109'; then + _err "The API-Key is invalid or could not be found" + else + _err "UNKNOWN API ERROR" + fi + return 1 + fi + return 0 +} diff --git a/dnsapi/dns_huaweicloud.sh b/dnsapi/dns_huaweicloud.sh new file mode 100644 index 00000000..b61c1d43 --- /dev/null +++ b/dnsapi/dns_huaweicloud.sh @@ -0,0 +1,327 @@ +#!/usr/bin/env sh + +# HUAWEICLOUD_Username +# HUAWEICLOUD_Password +# HUAWEICLOUD_DomainName + +iam_api="https://iam.myhuaweicloud.com" +dns_api="https://dns.ap-southeast-1.myhuaweicloud.com" # Should work + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +# +# Ref: https://support.huaweicloud.com/intl/zh-cn/api-dns/zh-cn_topic_0132421999.html +# +# About "DomainName" parameters see: https://support.huaweicloud.com/api-iam/iam_01_0006.html +# + +dns_huaweicloud_add() { + fulldomain=$1 + txtvalue=$2 + + HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}" + HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}" + HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}" + + # Check information + if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then + _err "Not enough information provided to dns_huaweicloud!" + return 1 + fi + + unset token # Clear token + token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")" + if [ -z "${token}" ]; then # Check token + _err "dns_api(dns_huaweicloud): Error getting token." + return 1 + fi + _secure_debug "Access token is:" "${token}" + + unset zoneid + zoneid="$(_get_zoneid "${token}" "${fulldomain}")" + if [ -z "${zoneid}" ]; then + _err "dns_api(dns_huaweicloud): Error getting zone id." + return 1 + fi + _debug "Zone ID is:" "${zoneid}" + + _debug "Adding Record" + _add_record "${token}" "${fulldomain}" "${txtvalue}" + ret="$?" + if [ "${ret}" != "0" ]; then + _err "dns_api(dns_huaweicloud): Error adding record." + return 1 + fi + + # Do saving work if all succeeded + _saveaccountconf_mutable HUAWEICLOUD_Username "${HUAWEICLOUD_Username}" + _saveaccountconf_mutable HUAWEICLOUD_Password "${HUAWEICLOUD_Password}" + _saveaccountconf_mutable HUAWEICLOUD_DomainName "${HUAWEICLOUD_DomainName}" + return 0 +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +# +# Ref: https://support.huaweicloud.com/intl/zh-cn/api-dns/dns_api_64005.html +# + +dns_huaweicloud_rm() { + fulldomain=$1 + txtvalue=$2 + + HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}" + HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}" + HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}" + + # Check information + if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then + _err "Not enough information provided to dns_huaweicloud!" + return 1 + fi + + unset token # Clear token + token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")" + if [ -z "${token}" ]; then # Check token + _err "dns_api(dns_huaweicloud): Error getting token." + return 1 + fi + _secure_debug "Access token is:" "${token}" + + unset zoneid + zoneid="$(_get_zoneid "${token}" "${fulldomain}")" + if [ -z "${zoneid}" ]; then + _err "dns_api(dns_huaweicloud): Error getting zone id." + return 1 + fi + _debug "Zone ID is:" "${zoneid}" + + record_id="$(_get_recordset_id "${token}" "${fulldomain}" "${zoneid}")" + _recursive_rm_record "${token}" "${fulldomain}" "${zoneid}" "${record_id}" + ret="$?" + if [ "${ret}" != "0" ]; then + _err "dns_api(dns_huaweicloud): Error removing record." + return 1 + fi + + return 0 +} + +################### Private functions below ################################## + +# _recursive_rm_record +# remove all records from the record set +# +# _token=$1 +# _domain=$2 +# _zoneid=$3 +# _record_id=$4 +# +# Returns 0 on success +_recursive_rm_record() { + _token=$1 + _domain=$2 + _zoneid=$3 + _record_id=$4 + + # Most likely to have problems will huaweicloud side if more than 50 attempts but still cannot fully remove the record set + # Maybe can be removed manually in the dashboard + _retry_cnt=50 + + # Remove all records + # Therotically HuaweiCloud does not allow more than one record set + # But remove them recurringly to increase robusty + + while [ "${_record_id}" != "0" ] && [ "${_retry_cnt}" != "0" ]; do + _debug "Removing Record" + _retry_cnt=$((_retry_cnt - 1)) + _rm_record "${_token}" "${_zoneid}" "${_record_id}" + _record_id="$(_get_recordset_id "${_token}" "${_domain}" "${_zoneid}")" + _debug2 "Checking record exists: record_id=${_record_id}" + done + + # Check if retry count is reached + if [ "${_retry_cnt}" = "0" ]; then + _debug "Failed to remove record after 50 attempts, please try removing it manually in the dashboard" + return 1 + fi + + return 0 +} + +# _get_zoneid +# +# _token=$1 +# _domain_string=$2 +# +# printf "%s" "${_zoneid}" +_get_zoneid() { + _token=$1 + _domain_string=$2 + export _H1="X-Auth-Token: ${_token}" + + i=1 + while true; do + h=$(printf "%s" "${_domain_string}" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + _debug "$h" + response=$(_get "${dns_api}/v2/zones?name=${h}") + _debug2 "$response" + if _contains "${response}" '"id"'; then + zoneidlist=$(echo "${response}" | _egrep_o "\"id\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | tr -d " ") + zonenamelist=$(echo "${response}" | _egrep_o "\"name\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | tr -d " ") + _debug2 "Returned Zone ID(s):" "${zoneidlist}" + _debug2 "Returned Zone Name(s):" "${zonenamelist}" + zoneidnum=0 + zoneidcount=$(echo "${zoneidlist}" | grep -c '^') + _debug "Returned Zone ID(s) Count:" "${zoneidcount}" + while [ "${zoneidnum}" -lt "${zoneidcount}" ]; do + zoneidnum=$(_math "$zoneidnum" + 1) + _zoneid=$(echo "${zoneidlist}" | sed -n "${zoneidnum}p") + zonename=$(echo "${zonenamelist}" | sed -n "${zoneidnum}p") + _debug "Check Zone Name" "${zonename}" + if [ "${zonename}" = "${h}." ]; then + _debug "Get Zone ID Success." + _debug "ZoneID:" "${_zoneid}" + printf "%s" "${_zoneid}" + return 0 + fi + done + fi + i=$(_math "$i" + 1) + done + return 1 +} + +_get_recordset_id() { + _token=$1 + _domain=$2 + _zoneid=$3 + export _H1="X-Auth-Token: ${_token}" + + response=$(_get "${dns_api}/v2/zones/${_zoneid}/recordsets?name=${_domain}") + if _contains "${response}" '"id"'; then + _id="$(echo "${response}" | _egrep_o "\"id\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | tr -d " ")" + printf "%s" "${_id}" + return 0 + fi + printf "%s" "0" + return 1 +} + +_add_record() { + _token=$1 + _domain=$2 + _txtvalue=$3 + + # Get Existing Records + export _H1="X-Auth-Token: ${_token}" + response=$(_get "${dns_api}/v2/zones/${zoneid}/recordsets?name=${_domain}") + + _debug2 "${response}" + _exist_record=$(echo "${response}" | _egrep_o '"records":[^]]*' | sed 's/\"records\"\:\[//g') + _debug "${_exist_record}" + + # Check if record exist + # Generate body data + if [ -z "${_exist_record}" ]; then + _post_body="{ + \"name\": \"${_domain}.\", + \"description\": \"ACME Challenge\", + \"type\": \"TXT\", + \"ttl\": 1, + \"records\": [ + \"\\\"${_txtvalue}\\\"\" + ] + }" + else + _post_body="{ + \"name\": \"${_domain}.\", + \"description\": \"ACME Challenge\", + \"type\": \"TXT\", + \"ttl\": 1, + \"records\": [ + ${_exist_record},\"\\\"${_txtvalue}\\\"\" + ] + }" + fi + + _record_id="$(_get_recordset_id "${_token}" "${_domain}" "${zoneid}")" + _debug "Record Set ID is:" "${_record_id}" + + # Add brand new records with all old and new records + export _H2="Content-Type: application/json" + export _H1="X-Auth-Token: ${_token}" + + _debug2 "${_post_body}" + if [ -z "${_exist_record}" ]; then + _post "${_post_body}" "${dns_api}/v2/zones/${zoneid}/recordsets" >/dev/null + else + _post "${_post_body}" "${dns_api}/v2/zones/${zoneid}/recordsets/${_record_id}" false "PUT" >/dev/null + fi + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + if [ "$_code" != "202" ]; then + _err "dns_huaweicloud: http code ${_code}" + return 1 + fi + return 0 +} + +# _rm_record $token $zoneid $recordid +# assume ${dns_api} exist +# no output +# return 0 +_rm_record() { + _token=$1 + _zone_id=$2 + _record_id=$3 + + export _H2="Content-Type: application/json" + export _H1="X-Auth-Token: ${_token}" + + _post "" "${dns_api}/v2/zones/${_zone_id}/recordsets/${_record_id}" false "DELETE" >/dev/null + return $? +} + +_get_token() { + _username=$1 + _password=$2 + _domain_name=$3 + + _debug "Getting Token" + body="{ + \"auth\": { + \"identity\": { + \"methods\": [ + \"password\" + ], + \"password\": { + \"user\": { + \"name\": \"${_username}\", + \"password\": \"${_password}\", + \"domain\": { + \"name\": \"${_domain_name}\" + } + } + } + }, + \"scope\": { + \"project\": { + \"name\": \"ap-southeast-1\" + } + } + } + }" + export _H1="Content-Type: application/json;charset=utf8" + _post "${body}" "${iam_api}/v3/auth/tokens" >/dev/null + _code=$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n") + _token=$(grep "^X-Subject-Token" "$HTTP_HEADER" | cut -d " " -f 2-) + _secure_debug "${_code}" + printf "%s" "${_token}" + return 0 +} diff --git a/dnsapi/dns_infoblox.sh b/dnsapi/dns_infoblox.sh index 4cbb2146..6bfd36ee 100644 --- a/dnsapi/dns_infoblox.sh +++ b/dnsapi/dns_infoblox.sh @@ -9,7 +9,6 @@ dns_infoblox_add() { ## Nothing to see here, just some housekeeping fulldomain=$1 txtvalue=$2 - baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=$Infoblox_View" _info "Using Infoblox API" _debug fulldomain "$fulldomain" @@ -19,12 +18,13 @@ dns_infoblox_add() { if [ -z "$Infoblox_Creds" ] || [ -z "$Infoblox_Server" ]; then Infoblox_Creds="" Infoblox_Server="" - _err "You didn't specify the credentials, server or infoblox view yet (Infoblox_Creds, Infoblox_Server and Infoblox_View)." - _err "Please set them via EXPORT ([username:password], [ip or hostname]) and try again." + _err "You didn't specify the Infoblox credentials or server (Infoblox_Creds; Infoblox_Server)." + _err "Please set them via EXPORT Infoblox_Creds=username:password or EXPORT Infoblox_server=ip/hostname and try again." return 1 fi if [ -z "$Infoblox_View" ]; then + _info "No Infoblox_View set, using fallback value 'default'" Infoblox_View="default" fi @@ -33,6 +33,9 @@ dns_infoblox_add() { _saveaccountconf Infoblox_Server "$Infoblox_Server" _saveaccountconf Infoblox_View "$Infoblox_View" + ## URLencode Infoblox View to deal with e.g. spaces + Infoblox_ViewEncoded=$(printf "%b" "$Infoblox_View" | _url_encode) + ## Base64 encode the credentials Infoblox_CredsEncoded=$(printf "%b" "$Infoblox_Creds" | _base64) @@ -40,11 +43,14 @@ dns_infoblox_add() { export _H1="Accept-Language:en-US" export _H2="Authorization: Basic $Infoblox_CredsEncoded" + ## Construct the request URL + baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=${Infoblox_ViewEncoded}" + ## Add the challenge record to the Infoblox grid member result="$(_post "" "$baseurlnObject" "" "POST")" ## Let's see if we get something intelligible back from the unit - if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then + if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/${Infoblox_ViewEncoded}")" ]; then _info "Successfully created the txt record" return 0 else @@ -65,6 +71,9 @@ dns_infoblox_rm() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" + ## URLencode Infoblox View to deal with e.g. spaces + Infoblox_ViewEncoded=$(printf "%b" "$Infoblox_View" | _url_encode) + ## Base64 encode the credentials Infoblox_CredsEncoded="$(printf "%b" "$Infoblox_Creds" | _base64)" @@ -73,18 +82,18 @@ dns_infoblox_rm() { export _H2="Authorization: Basic $Infoblox_CredsEncoded" ## Does the record exist? Let's check. - baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=$Infoblox_View&_return_type=xml-pretty" + baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&view=${Infoblox_ViewEncoded}&_return_type=xml-pretty" result="$(_get "$baseurlnObject")" ## Let's see if we get something intelligible back from the grid - if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then + if [ "$(echo "$result" | _egrep_o "record:txt/.*:.*/${Infoblox_ViewEncoded}")" ]; then ## Extract the object reference - objRef="$(printf "%b" "$result" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" + objRef="$(printf "%b" "$result" | _egrep_o "record:txt/.*:.*/${Infoblox_ViewEncoded}")" objRmUrl="https://$Infoblox_Server/wapi/v2.2.2/$objRef" ## Delete them! All the stale records! rmResult="$(_post "" "$objRmUrl" "" "DELETE")" ## Let's see if that worked - if [ "$(echo "$rmResult" | _egrep_o "record:txt/.*:.*/$Infoblox_View")" ]; then + if [ "$(echo "$rmResult" | _egrep_o "record:txt/.*:.*/${Infoblox_ViewEncoded}")" ]; then _info "Successfully deleted $objRef" return 0 else diff --git a/dnsapi/dns_infomaniak.sh b/dnsapi/dns_infomaniak.sh new file mode 100755 index 00000000..a005132c --- /dev/null +++ b/dnsapi/dns_infomaniak.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env sh + +############################################################################### +# Infomaniak API integration +# +# To use this API you need visit the API dashboard of your account +# once logged into https://manager.infomaniak.com add /api/dashboard to the URL +# +# Please report bugs to +# https://github.com/acmesh-official/acme.sh/issues/3188 +# +# Note: the URL looks like this: +# https://manager.infomaniak.com/v3//api/dashboard +# Then generate a token with the scope Domain +# this is given as an environment variable INFOMANIAK_API_TOKEN +############################################################################### + +# base variables + +DEFAULT_INFOMANIAK_API_URL="https://api.infomaniak.com" +DEFAULT_INFOMANIAK_TTL=300 + +######## Public functions ##################### + +#Usage: dns_infomaniak_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_infomaniak_add() { + + INFOMANIAK_API_TOKEN="${INFOMANIAK_API_TOKEN:-$(_readaccountconf_mutable INFOMANIAK_API_TOKEN)}" + INFOMANIAK_API_URL="${INFOMANIAK_API_URL:-$(_readaccountconf_mutable INFOMANIAK_API_URL)}" + INFOMANIAK_TTL="${INFOMANIAK_TTL:-$(_readaccountconf_mutable INFOMANIAK_TTL)}" + + if [ -z "$INFOMANIAK_API_TOKEN" ]; then + INFOMANIAK_API_TOKEN="" + _err "Please provide a valid Infomaniak API token in variable INFOMANIAK_API_TOKEN" + return 1 + fi + + if [ -z "$INFOMANIAK_API_URL" ]; then + INFOMANIAK_API_URL="$DEFAULT_INFOMANIAK_API_URL" + fi + + if [ -z "$INFOMANIAK_TTL" ]; then + INFOMANIAK_TTL="$DEFAULT_INFOMANIAK_TTL" + fi + + #save the token to the account conf file. + _saveaccountconf_mutable INFOMANIAK_API_TOKEN "$INFOMANIAK_API_TOKEN" + + if [ "$INFOMANIAK_API_URL" != "$DEFAULT_INFOMANIAK_API_URL" ]; then + _saveaccountconf_mutable INFOMANIAK_API_URL "$INFOMANIAK_API_URL" + fi + + if [ "$INFOMANIAK_TTL" != "$DEFAULT_INFOMANIAK_TTL" ]; then + _saveaccountconf_mutable INFOMANIAK_TTL "$INFOMANIAK_TTL" + fi + + export _H1="Authorization: Bearer $INFOMANIAK_API_TOKEN" + export _H2="Content-Type: application/json" + + fulldomain="$1" + txtvalue="$2" + + _info "Infomaniak DNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + fqdn=${fulldomain#_acme-challenge.} + + # guess which base domain to add record to + zone_and_id=$(_find_zone "$fqdn") + if [ -z "$zone_and_id" ]; then + _err "cannot find zone to modify" + return 1 + fi + zone=${zone_and_id% *} + domain_id=${zone_and_id#* } + + # extract first part of domain + key=${fulldomain%."$zone"} + + _debug "zone:$zone id:$domain_id key:$key" + + # payload + data="{\"type\": \"TXT\", \"source\": \"$key\", \"target\": \"$txtvalue\", \"ttl\": $INFOMANIAK_TTL}" + + # API call + response=$(_post "$data" "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record") + if [ -n "$response" ] && echo "$response" | _contains '"result":"success"'; then + _info "Record added" + _debug "Response: $response" + return 0 + fi + _err "could not create record" + _debug "Response: $response" + return 1 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_infomaniak_rm() { + + INFOMANIAK_API_TOKEN="${INFOMANIAK_API_TOKEN:-$(_readaccountconf_mutable INFOMANIAK_API_TOKEN)}" + INFOMANIAK_API_URL="${INFOMANIAK_API_URL:-$(_readaccountconf_mutable INFOMANIAK_API_URL)}" + INFOMANIAK_TTL="${INFOMANIAK_TTL:-$(_readaccountconf_mutable INFOMANIAK_TTL)}" + + if [ -z "$INFOMANIAK_API_TOKEN" ]; then + INFOMANIAK_API_TOKEN="" + _err "Please provide a valid Infomaniak API token in variable INFOMANIAK_API_TOKEN" + return 1 + fi + + if [ -z "$INFOMANIAK_API_URL" ]; then + INFOMANIAK_API_URL="$DEFAULT_INFOMANIAK_API_URL" + fi + + if [ -z "$INFOMANIAK_TTL" ]; then + INFOMANIAK_TTL="$DEFAULT_INFOMANIAK_TTL" + fi + + #save the token to the account conf file. + _saveaccountconf_mutable INFOMANIAK_API_TOKEN "$INFOMANIAK_API_TOKEN" + + if [ "$INFOMANIAK_API_URL" != "$DEFAULT_INFOMANIAK_API_URL" ]; then + _saveaccountconf_mutable INFOMANIAK_API_URL "$INFOMANIAK_API_URL" + fi + + if [ "$INFOMANIAK_TTL" != "$DEFAULT_INFOMANIAK_TTL" ]; then + _saveaccountconf_mutable INFOMANIAK_TTL "$INFOMANIAK_TTL" + fi + + export _H1="Authorization: Bearer $INFOMANIAK_API_TOKEN" + export _H2="ContentType: application/json" + + fulldomain=$1 + txtvalue=$2 + _info "Infomaniak DNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + fqdn=${fulldomain#_acme-challenge.} + + # guess which base domain to add record to + zone_and_id=$(_find_zone "$fqdn") + if [ -z "$zone_and_id" ]; then + _err "cannot find zone to modify" + return 1 + fi + zone=${zone_and_id% *} + domain_id=${zone_and_id#* } + + # extract first part of domain + key=${fulldomain%."$zone"} + + _debug "zone:$zone id:$domain_id key:$key" + + # find previous record + # shellcheck disable=SC1004 + record_id=$(_get "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record" | sed 's/.*"data":\[\(.*\)\]}/\1/; s/},{/}\ +{/g' | sed -n 's/.*"id":"*\([0-9]*\)"*.*"source_idn":"'"$fulldomain"'".*"target_idn":"'"$txtvalue"'".*/\1/p') + if [ -z "$record_id" ]; then + _err "could not find record to delete" + return 1 + fi + _debug "record_id: $record_id" + + # API call + response=$(_post "" "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record/$record_id" "" DELETE) + if [ -n "$response" ] && echo "$response" | _contains '"result":"success"'; then + _info "Record deleted" + return 0 + fi + _err "could not delete record" + return 1 +} + +#################### Private functions below ################################## + +_get_domain_id() { + domain="$1" + + # shellcheck disable=SC1004 + _get "${INFOMANIAK_API_URL}/1/product?service_name=domain&customer_name=$domain" | sed 's/.*"data":\[{\(.*\)}\]}/\1/; s/,/\ +/g' | sed -n 's/^"id":\(.*\)/\1/p' +} + +_find_zone() { + zone="$1" + + # find domain in list, removing . parts sequentialy + while _contains "$zone" '\.'; do + _debug "testing $zone" + id=$(_get_domain_id "$zone") + if [ -n "$id" ]; then + echo "$zone $id" + return + fi + zone=${zone#*.} + done +} diff --git a/dnsapi/dns_internetbs.sh b/dnsapi/dns_internetbs.sh new file mode 100755 index 00000000..ae6b9e1e --- /dev/null +++ b/dnsapi/dns_internetbs.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env sh + +#This is the Internet.BS api wrapper for acme.sh +# +#Author: Ne-Lexa +#Report Bugs here: https://github.com/Ne-Lexa/acme.sh + +#INTERNETBS_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje" +#INTERNETBS_API_PASSWORD="sdfsdfsdfljlbjkljlkjsdfoiwje" + +INTERNETBS_API_URL="https://api.internet.bs" + +######## Public functions ##################### + +#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_internetbs_add() { + fulldomain=$1 + txtvalue=$2 + + INTERNETBS_API_KEY="${INTERNETBS_API_KEY:-$(_readaccountconf_mutable INTERNETBS_API_KEY)}" + INTERNETBS_API_PASSWORD="${INTERNETBS_API_PASSWORD:-$(_readaccountconf_mutable INTERNETBS_API_PASSWORD)}" + + if [ -z "$INTERNETBS_API_KEY" ] || [ -z "$INTERNETBS_API_PASSWORD" ]; then + INTERNETBS_API_KEY="" + INTERNETBS_API_PASSWORD="" + _err "You didn't specify the INTERNET.BS api key and password yet." + _err "Please create you key and try again." + return 1 + fi + + _saveaccountconf_mutable INTERNETBS_API_KEY "$INTERNETBS_API_KEY" + _saveaccountconf_mutable INTERNETBS_API_PASSWORD "$INTERNETBS_API_PASSWORD" + + _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" + + # https://testapi.internet.bs/Domain/DnsRecord/Add?ApiKey=testapi&Password=testpass&FullRecordName=w3.test-api-domain7.net&Type=CNAME&Value=www.internet.bs%&ResponseFormat=json + if _internetbs_rest POST "Domain/DnsRecord/Add" "FullRecordName=${_sub_domain}.${_domain}&Type=TXT&Value=${txtvalue}&ResponseFormat=json"; then + if ! _contains "$response" "\"status\":\"SUCCESS\""; then + _err "ERROR add TXT record" + _err "$response" + return 1 + fi + + _info "txt record add success." + return 0 + fi + + return 1 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_internetbs_rm() { + fulldomain=$1 + txtvalue=$2 + + INTERNETBS_API_KEY="${INTERNETBS_API_KEY:-$(_readaccountconf_mutable INTERNETBS_API_KEY)}" + INTERNETBS_API_PASSWORD="${INTERNETBS_API_PASSWORD:-$(_readaccountconf_mutable INTERNETBS_API_PASSWORD)}" + + if [ -z "$INTERNETBS_API_KEY" ] || [ -z "$INTERNETBS_API_PASSWORD" ]; then + INTERNETBS_API_KEY="" + INTERNETBS_API_PASSWORD="" + _err "You didn't specify the INTERNET.BS api key and password yet." + _err "Please create you key and try again." + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + # https://testapi.internet.bs/Domain/DnsRecord/List?ApiKey=testapi&Password=testpass&Domain=test-api-domain7.net&FilterType=CNAME&ResponseFormat=json + _internetbs_rest POST "Domain/DnsRecord/List" "Domain=$_domain&FilterType=TXT&ResponseFormat=json" + + if ! _contains "$response" "\"status\":\"SUCCESS\""; then + _err "ERROR list dns records" + _err "$response" + return 1 + fi + + if _contains "$response" "\name\":\"${_sub_domain}.${_domain}\""; then + _info "txt record find." + + # https://testapi.internet.bs/Domain/DnsRecord/Remove?ApiKey=testapi&Password=testpass&FullRecordName=www.test-api-domain7.net&Type=cname&ResponseFormat=json + _internetbs_rest POST "Domain/DnsRecord/Remove" "FullRecordName=${_sub_domain}.${_domain}&Type=TXT&ResponseFormat=json" + + if ! _contains "$response" "\"status\":\"SUCCESS\""; then + _err "ERROR remove dns record" + _err "$response" + return 1 + fi + + _info "txt record deleted success." + return 0 + fi + + return 1 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=12345 +_get_root() { + domain=$1 + i=2 + p=1 + + # https://testapi.internet.bs/Domain/List?ApiKey=testapi&Password=testpass&CompactList=yes&ResponseFormat=json + if _internetbs_rest POST "Domain/List" "CompactList=yes&ResponseFormat=json"; then + + if ! _contains "$response" "\"status\":\"SUCCESS\""; then + _err "ERROR fetch domain list" + _err "$response" + 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" "\"$h\""; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-${p}) + _domain=${h} + return 0 + fi + + p=${i} + i=$(_math "$i" + 1) + done + fi + return 1 +} + +#Usage: method URI data +_internetbs_rest() { + m="$1" + ep="$2" + data="$3" + url="${INTERNETBS_API_URL}/${ep}" + + _debug url "$url" + + apiKey="$(printf "%s" "${INTERNETBS_API_KEY}" | _url_encode)" + password="$(printf "%s" "${INTERNETBS_API_PASSWORD}" | _url_encode)" + + if [ "$m" = "GET" ]; then + response="$(_get "${url}?ApiKey=${apiKey}&Password=${password}&${data}" | tr -d '\r')" + else + _debug2 data "$data" + response="$(_post "$data" "${url}?ApiKey=${apiKey}&Password=${password}" | tr -d '\r')" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index cd5af91b..e483c0e8 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -34,6 +34,10 @@ dns_inwx_add() { _saveaccountconf_mutable INWX_Password "$INWX_Password" _saveaccountconf_mutable INWX_Shared_Secret "$INWX_Shared_Secret" + if ! _inwx_login; then + return 1 + fi + _debug "First detect the root zone" if ! _get_root "$fulldomain"; then _err "invalid domain" @@ -55,6 +59,7 @@ dns_inwx_rm() { INWX_User="${INWX_User:-$(_readaccountconf_mutable INWX_User)}" INWX_Password="${INWX_Password:-$(_readaccountconf_mutable INWX_Password)}" + INWX_Shared_Secret="${INWX_Shared_Secret:-$(_readaccountconf_mutable INWX_Shared_Secret)}" if [ -z "$INWX_User" ] || [ -z "$INWX_Password" ]; then INWX_User="" INWX_Password="" @@ -63,9 +68,9 @@ dns_inwx_rm() { return 1 fi - #save the api key and email to the account conf file. - _saveaccountconf_mutable INWX_User "$INWX_User" - _saveaccountconf_mutable INWX_Password "$INWX_Password" + if ! _inwx_login; then + return 1 + fi _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -126,8 +131,42 @@ dns_inwx_rm() { #################### Private functions below ################################## +_inwx_check_cookie() { + INWX_Cookie="${INWX_Cookie:-$(_readaccountconf_mutable INWX_Cookie)}" + if [ -z "$INWX_Cookie" ]; then + _debug "No cached cookie found" + return 1 + fi + _H1="$INWX_Cookie" + export _H1 + + xml_content=$(printf ' + + account.info + ') + + response="$(_post "$xml_content" "$INWX_Api" "" "POST")" + + if _contains "$response" "code1000"; then + _debug "Cached cookie still valid" + return 0 + fi + + _debug "Cached cookie no longer valid" + _H1="" + export _H1 + INWX_Cookie="" + _saveaccountconf_mutable INWX_Cookie "$INWX_Cookie" + return 1 +} + _inwx_login() { + if _inwx_check_cookie; then + _debug "Already logged in" + return 0 + fi + xml_content=$(printf ' account.login @@ -151,16 +190,25 @@ _inwx_login() { - ' $INWX_User $INWX_Password) + ' "$INWX_User" "$INWX_Password") response="$(_post "$xml_content" "$INWX_Api" "" "POST")" - _H1=$(printf "Cookie: %s" "$(grep "domrobot=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'domrobot=[^;]*;' | tr -d ';')") + + INWX_Cookie=$(printf "Cookie: %s" "$(grep "domrobot=" "$HTTP_HEADER" | grep -i "^Set-Cookie:" | _tail_n 1 | _egrep_o 'domrobot=[^;]*;' | tr -d ';')") + _H1=$INWX_Cookie export _H1 + export INWX_Cookie + _saveaccountconf_mutable INWX_Cookie "$INWX_Cookie" + + if ! _contains "$response" "code1000"; then + _err "INWX API: Authentication error (username/password correct?)" + return 1 + fi #https://github.com/inwx/php-client/blob/master/INWX/Domrobot.php#L71 - if _contains "$response" "tfa"; then + if _contains "$response" "tfaGOOGLE-AUTH"; then if [ -z "$INWX_Shared_Secret" ]; then - _err "Mobile TAN detected." + _err "INWX API: Mobile TAN detected." _err "Please define a shared secret." return 1 fi @@ -193,6 +241,11 @@ _inwx_login() { ' "$tan") response="$(_post "$xml_content" "$INWX_Api" "" "POST")" + + if ! _contains "$response" "code1000"; then + _err "INWX API: Mobile TAN not correct." + return 1 + fi fi } @@ -205,11 +258,23 @@ _get_root() { i=2 p=1 - _inwx_login - xml_content=' nameserver.list + + + + + + pagelimit + + 9999 + + + + + + ' response="$(_post "$xml_content" "$INWX_Api" "" "POST")" diff --git a/dnsapi/dns_ionos.sh b/dnsapi/dns_ionos.sh new file mode 100755 index 00000000..e4ad3318 --- /dev/null +++ b/dnsapi/dns_ionos.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env sh + +# Supports IONOS DNS API v1.0.1 +# +# Usage: +# Export IONOS_PREFIX and IONOS_SECRET before calling acme.sh: +# +# $ export IONOS_PREFIX="..." +# $ export IONOS_SECRET="..." +# +# $ acme.sh --issue --dns dns_ionos ... + +IONOS_API="https://api.hosting.ionos.com/dns" +IONOS_ROUTE_ZONES="/v1/zones" + +IONOS_TXT_TTL=60 # minimum accepted by API +IONOS_TXT_PRIO=10 + +dns_ionos_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _ionos_init; then + return 1 + fi + + _body="[{\"name\":\"$_sub_domain.$_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":$IONOS_TXT_TTL,\"prio\":$IONOS_TXT_PRIO,\"disabled\":false}]" + + if _ionos_rest POST "$IONOS_ROUTE_ZONES/$_zone_id/records" "$_body" && [ "$_code" = "201" ]; then + _info "TXT record has been created successfully." + return 0 + fi + + return 1 +} + +dns_ionos_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _ionos_init; then + return 1 + fi + + if ! _ionos_get_record "$fulldomain" "$_zone_id" "$txtvalue"; then + _err "Could not find _acme-challenge TXT record." + return 1 + fi + + if _ionos_rest DELETE "$IONOS_ROUTE_ZONES/$_zone_id/records/$_record_id" && [ "$_code" = "200" ]; then + _info "TXT record has been deleted successfully." + return 0 + fi + + return 1 +} + +_ionos_init() { + IONOS_PREFIX="${IONOS_PREFIX:-$(_readaccountconf_mutable IONOS_PREFIX)}" + IONOS_SECRET="${IONOS_SECRET:-$(_readaccountconf_mutable IONOS_SECRET)}" + + if [ -z "$IONOS_PREFIX" ] || [ -z "$IONOS_SECRET" ]; then + _err "You didn't specify an IONOS api prefix and secret yet." + _err "Read https://beta.developer.hosting.ionos.de/docs/getstarted to learn how to get a prefix and secret." + _err "" + _err "Then set them before calling acme.sh:" + _err "\$ export IONOS_PREFIX=\"...\"" + _err "\$ export IONOS_SECRET=\"...\"" + _err "\$ acme.sh --issue -d ... --dns dns_ionos" + return 1 + fi + + _saveaccountconf_mutable IONOS_PREFIX "$IONOS_PREFIX" + _saveaccountconf_mutable IONOS_SECRET "$IONOS_SECRET" + + if ! _get_root "$fulldomain"; then + _err "Cannot find this domain in your IONOS account." + return 1 + fi +} + +_get_root() { + domain=$1 + i=1 + p=1 + + if _ionos_rest GET "$IONOS_ROUTE_ZONES"; then + _response="$(echo "$_response" | tr -d "\n")" + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + return 1 + fi + + _zone="$(echo "$_response" | _egrep_o "\"name\":\"$h\".*\}")" + if [ "$_zone" ]; then + _zone_id=$(printf "%s\n" "$_zone" | _egrep_o "\"id\":\"[a-fA-F0-9\-]*\"" | _head_n 1 | cut -d : -f 2 | tr -d '\"') + if [ "$_zone_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + + return 0 + fi + + return 1 + fi + + p=$i + i=$(_math "$i" + 1) + done + fi + + return 1 +} + +_ionos_get_record() { + fulldomain=$1 + zone_id=$2 + txtrecord=$3 + + if _ionos_rest GET "$IONOS_ROUTE_ZONES/$zone_id?recordName=$fulldomain&recordType=TXT"; then + _response="$(echo "$_response" | tr -d "\n")" + + _record="$(echo "$_response" | _egrep_o "\"name\":\"$fulldomain\"[^\}]*\"type\":\"TXT\"[^\}]*\"content\":\"\\\\\"$txtrecord\\\\\"\".*\}")" + if [ "$_record" ]; then + _record_id=$(printf "%s\n" "$_record" | _egrep_o "\"id\":\"[a-fA-F0-9\-]*\"" | _head_n 1 | cut -d : -f 2 | tr -d '\"') + + return 0 + fi + fi + + return 1 +} + +_ionos_rest() { + method="$1" + route="$2" + data="$3" + + IONOS_API_KEY="$(printf "%s.%s" "$IONOS_PREFIX" "$IONOS_SECRET")" + + export _H1="X-API-Key: $IONOS_API_KEY" + + # clear headers + : >"$HTTP_HEADER" + + if [ "$method" != "GET" ]; then + export _H2="Accept: application/json" + export _H3="Content-Type: application/json" + + _response="$(_post "$data" "$IONOS_API$route" "" "$method" "application/json")" + else + export _H2="Accept: */*" + export _H3= + + _response="$(_get "$IONOS_API$route")" + fi + + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + + if [ "$?" != "0" ]; then + _err "Error $route: $_response" + return 1 + fi + + _debug2 "_response" "$_response" + _debug2 "_code" "$_code" + + return 0 +} diff --git a/dnsapi/dns_ipv64.sh b/dnsapi/dns_ipv64.sh new file mode 100755 index 00000000..54470119 --- /dev/null +++ b/dnsapi/dns_ipv64.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env sh + +#Created by Roman Lumetsberger, to use ipv64.net's API to add/remove text records +#2022/11/29 + +# Pass credentials before "acme.sh --issue --dns dns_ipv64 ..." +# -- +# export IPv64_Token="aaaaaaaaaaaaaaaaaaaaaaaaaa" +# -- +# + +IPv64_API="https://ipv64.net/api" + +######## Public functions ###################### + +#Usage: dns_ipv64_add _acme-challenge.domain.ipv64.net "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_ipv64_add() { + fulldomain=$1 + txtvalue=$2 + + IPv64_Token="${IPv64_Token:-$(_readaccountconf_mutable IPv64_Token)}" + if [ -z "$IPv64_Token" ]; then + _err "You must export variable: IPv64_Token" + _err "The API Key for your IPv64 account is necessary." + _err "You can look it up in your IPv64 account." + return 1 + fi + + # Now save the credentials. + _saveaccountconf_mutable IPv64_Token "$IPv64_Token" + + if ! _get_root "$fulldomain"; then + _err "invalid domain" "$fulldomain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + # convert to lower case + _domain="$(echo "$_domain" | _lower_case)" + _sub_domain="$(echo "$_sub_domain" | _lower_case)" + # Now add the TXT record + _info "Trying to add TXT record" + if _ipv64_rest "POST" "add_record=$_domain&praefix=$_sub_domain&type=TXT&content=$txtvalue"; then + _info "TXT record has been successfully added." + return 0 + else + _err "Errors happened during adding the TXT record, response=$_response" + return 1 + fi + +} + +#Usage: fulldomain txtvalue +#Usage: dns_ipv64_rm _acme-challenge.domain.ipv64.net "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +#Remove the txt record after validation. +dns_ipv64_rm() { + fulldomain=$1 + txtvalue=$2 + + IPv64_Token="${IPv64_Token:-$(_readaccountconf_mutable IPv64_Token)}" + if [ -z "$IPv64_Token" ]; then + _err "You must export variable: IPv64_Token" + _err "The API Key for your IPv64 account is necessary." + _err "You can look it up in your IPv64 account." + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "invalid domain" "$fulldomain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + # convert to lower case + _domain="$(echo "$_domain" | _lower_case)" + _sub_domain="$(echo "$_sub_domain" | _lower_case)" + # Now delete the TXT record + _info "Trying to delete TXT record" + if _ipv64_rest "DELETE" "del_record=$_domain&praefix=$_sub_domain&type=TXT&content=$txtvalue"; then + _info "TXT record has been successfully deleted." + return 0 + else + _err "Errors happened during deleting the TXT record, response=$_response" + return 1 + fi + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain="$1" + i=1 + p=1 + + _ipv64_get "get_domains" + domain_data=$_response + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + #if _contains "$domain_data" "\""$h"\"\:"; then + if _contains "$domain_data" "\"""$h""\"\:"; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain="$h" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +#send get request to api +# $1 has to set the api-function +_ipv64_get() { + url="$IPv64_API?$1" + export _H1="Authorization: Bearer $IPv64_Token" + + _response=$(_get "$url") + _response="$(echo "$_response" | _normalizeJson)" + + if _contains "$_response" "429 Too Many Requests"; then + _info "API throttled, sleeping to reset the limit" + _sleep 10 + _response=$(_get "$url") + _response="$(echo "$_response" | _normalizeJson)" + fi +} + +_ipv64_rest() { + url="$IPv64_API" + export _H1="Authorization: Bearer $IPv64_Token" + export _H2="Content-Type: application/x-www-form-urlencoded" + _response=$(_post "$2" "$url" "" "$1") + + if _contains "$_response" "429 Too Many Requests"; then + _info "API throttled, sleeping to reset the limit" + _sleep 10 + _response=$(_post "$2" "$url" "" "$1") + fi + + if ! _contains "$_response" "\"info\":\"success\""; then + return 1 + fi + _debug2 response "$_response" + return 0 +} diff --git a/dnsapi/dns_ispconfig.sh b/dnsapi/dns_ispconfig.sh index 2d8d6b0a..560f073e 100755 --- a/dnsapi/dns_ispconfig.sh +++ b/dnsapi/dns_ispconfig.sh @@ -32,7 +32,11 @@ dns_ispconfig_rm() { #################### Private functions below ################################## _ISPC_credentials() { - if [ -z "${ISPC_User}" ] || [ -z "$ISPC_Password" ] || [ -z "${ISPC_Api}" ] || [ -z "${ISPC_Api_Insecure}" ]; then + ISPC_User="${ISPC_User:-$(_readaccountconf_mutable ISPC_User)}" + ISPC_Password="${ISPC_Password:-$(_readaccountconf_mutable ISPC_Password)}" + ISPC_Api="${ISPC_Api:-$(_readaccountconf_mutable ISPC_Api)}" + ISPC_Api_Insecure="${ISPC_Api_Insecure:-$(_readaccountconf_mutable ISPC_Api_Insecure)}" + if [ -z "${ISPC_User}" ] || [ -z "${ISPC_Password}" ] || [ -z "${ISPC_Api}" ] || [ -z "${ISPC_Api_Insecure}" ]; then ISPC_User="" ISPC_Password="" ISPC_Api="" @@ -40,10 +44,10 @@ _ISPC_credentials() { _err "You haven't specified the ISPConfig Login data, URL and whether you want check the ISPC SSL cert. Please try again." return 1 else - _saveaccountconf ISPC_User "${ISPC_User}" - _saveaccountconf ISPC_Password "${ISPC_Password}" - _saveaccountconf ISPC_Api "${ISPC_Api}" - _saveaccountconf ISPC_Api_Insecure "${ISPC_Api_Insecure}" + _saveaccountconf_mutable ISPC_User "${ISPC_User}" + _saveaccountconf_mutable ISPC_Password "${ISPC_Password}" + _saveaccountconf_mutable ISPC_Api "${ISPC_Api}" + _saveaccountconf_mutable ISPC_Api_Insecure "${ISPC_Api_Insecure}" # Set whether curl should use secure or insecure mode export HTTPS_INSECURE="${ISPC_Api_Insecure}" fi @@ -75,7 +79,7 @@ _ISPC_getZoneInfo() { # suffix . needed for zone -> domain.tld. curData="{\"session_id\":\"${sessionID}\",\"primary_id\":{\"origin\":\"${curZone}.\"}}" curResult="$(_post "${curData}" "${ISPC_Api}?dns_zone_get")" - _debug "Calling _ISPC_getZoneInfo: '${curData}' '${ISPC_Api}?login'" + _debug "Calling _ISPC_getZoneInfo: '${curData}' '${ISPC_Api}?dns_zone_get'" _debug "Result of _ISPC_getZoneInfo: '$curResult'" if _contains "${curResult}" '"id":"'; then zoneFound=true @@ -95,33 +99,47 @@ _ISPC_getZoneInfo() { server_id=$(echo "${curResult}" | _egrep_o "server_id.*" | cut -d ':' -f 2 | cut -d '"' -f 2) _debug "Server ID: '${server_id}'" case "${server_id}" in - '' | *[!0-9]*) - _err "Server ID is not numeric." - return 1 - ;; - *) _info "Retrieved Server ID" ;; + '' | *[!0-9]*) + _err "Server ID is not numeric." + return 1 + ;; + *) _info "Retrieved Server ID" ;; esac zone=$(echo "${curResult}" | _egrep_o "\"id.*" | cut -d ':' -f 2 | cut -d '"' -f 2) _debug "Zone: '${zone}'" case "${zone}" in - '' | *[!0-9]*) - _err "Zone ID is not numeric." - return 1 - ;; - *) _info "Retrieved Zone ID" ;; + '' | *[!0-9]*) + _err "Zone ID is not numeric." + return 1 + ;; + *) _info "Retrieved Zone ID" ;; esac - client_id=$(echo "${curResult}" | _egrep_o "sys_userid.*" | cut -d ':' -f 2 | cut -d '"' -f 2) - _debug "Client ID: '${client_id}'" - case "${client_id}" in - '' | *[!0-9]*) - _err "Client ID is not numeric." - return 1 - ;; - *) _info "Retrieved Client ID." ;; + sys_userid=$(echo "${curResult}" | _egrep_o "sys_userid.*" | cut -d ':' -f 2 | cut -d '"' -f 2) + _debug "SYS User ID: '${sys_userid}'" + case "${sys_userid}" in + '' | *[!0-9]*) + _err "SYS User ID is not numeric." + return 1 + ;; + *) _info "Retrieved SYS User ID." ;; esac zoneFound="" zoneEnd="" fi + # Need to get client_id as it is different from sys_userid + curData="{\"session_id\":\"${sessionID}\",\"sys_userid\":\"${sys_userid}\"}" + curResult="$(_post "${curData}" "${ISPC_Api}?client_get_id")" + _debug "Calling _ISPC_ClientGetID: '${curData}' '${ISPC_Api}?client_get_id'" + _debug "Result of _ISPC_ClientGetID: '$curResult'" + client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | tr -d '{}') + _debug "Client ID: '${client_id}'" + case "${client_id}" in + '' | *[!0-9]*) + _err "Client ID is not numeric." + return 1 + ;; + *) _info "Retrieved Client ID." ;; + esac } _ISPC_addTxt() { @@ -135,11 +153,11 @@ _ISPC_addTxt() { record_id=$(echo "${curResult}" | _egrep_o "\"response.*" | cut -d ':' -f 2 | cut -d '"' -f 2) _debug "Record ID: '${record_id}'" case "${record_id}" in - '' | *[!0-9]*) - _err "Couldn't add ACME Challenge TXT record to zone." - return 1 - ;; - *) _info "Added ACME Challenge TXT record to zone." ;; + '' | *[!0-9]*) + _err "Couldn't add ACME Challenge TXT record to zone." + return 1 + ;; + *) _info "Added ACME Challenge TXT record to zone." ;; esac } @@ -153,24 +171,24 @@ _ISPC_rmTxt() { record_id=$(echo "${curResult}" | _egrep_o "\"id.*" | cut -d ':' -f 2 | cut -d '"' -f 2) _debug "Record ID: '${record_id}'" case "${record_id}" in - '' | *[!0-9]*) - _err "Record ID is not numeric." + '' | *[!0-9]*) + _err "Record ID is not numeric." + return 1 + ;; + *) + unset IFS + _info "Retrieved Record ID." + curData="{\"session_id\":\"${sessionID}\",\"primary_id\":\"${record_id}\",\"update_serial\":true}" + curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_delete")" + _debug "Calling _ISPC_rmTxt: '${curData}' '${ISPC_Api}?dns_txt_delete'" + _debug "Result of _ISPC_rmTxt: '$curResult'" + if _contains "${curResult}" '"code":"ok"'; then + _info "Removed ACME Challenge TXT record from zone." + else + _err "Couldn't remove ACME Challenge TXT record from zone." return 1 - ;; - *) - unset IFS - _info "Retrieved Record ID." - curData="{\"session_id\":\"${sessionID}\",\"primary_id\":\"${record_id}\",\"update_serial\":true}" - curResult="$(_post "${curData}" "${ISPC_Api}?dns_txt_delete")" - _debug "Calling _ISPC_rmTxt: '${curData}' '${ISPC_Api}?dns_txt_delete'" - _debug "Result of _ISPC_rmTxt: '$curResult'" - if _contains "${curResult}" '"code":"ok"'; then - _info "Removed ACME Challenge TXT record from zone." - else - _err "Couldn't remove ACME Challenge TXT record from zone." - return 1 - fi - ;; + fi + ;; esac fi } diff --git a/dnsapi/dns_jd.sh b/dnsapi/dns_jd.sh new file mode 100644 index 00000000..d0f2a501 --- /dev/null +++ b/dnsapi/dns_jd.sh @@ -0,0 +1,286 @@ +#!/usr/bin/env sh + +# +#JD_ACCESS_KEY_ID="sdfsdfsdfljlbjkljlkjsdfoiwje" +#JD_ACCESS_KEY_SECRET="xxxxxxx" +#JD_REGION="cn-north-1" + +_JD_ACCOUNT="https://uc.jdcloud.com/account/accesskey" + +_JD_PROD="clouddnsservice" +_JD_API="jdcloud-api.com" + +_JD_API_VERSION="v1" +_JD_DEFAULT_REGION="cn-north-1" + +_JD_HOST="$_JD_PROD.$_JD_API" + +######## Public functions ##################### + +#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_jd_add() { + fulldomain=$1 + txtvalue=$2 + + JD_ACCESS_KEY_ID="${JD_ACCESS_KEY_ID:-$(_readaccountconf_mutable JD_ACCESS_KEY_ID)}" + JD_ACCESS_KEY_SECRET="${JD_ACCESS_KEY_SECRET:-$(_readaccountconf_mutable JD_ACCESS_KEY_SECRET)}" + JD_REGION="${JD_REGION:-$(_readaccountconf_mutable JD_REGION)}" + + if [ -z "$JD_ACCESS_KEY_ID" ] || [ -z "$JD_ACCESS_KEY_SECRET" ]; then + JD_ACCESS_KEY_ID="" + JD_ACCESS_KEY_SECRET="" + _err "You haven't specifed the jdcloud api key id or api key secret yet." + _err "Please create your key and try again. see $(__green $_JD_ACCOUNT)" + return 1 + fi + + _saveaccountconf_mutable JD_ACCESS_KEY_ID "$JD_ACCESS_KEY_ID" + _saveaccountconf_mutable JD_ACCESS_KEY_SECRET "$JD_ACCESS_KEY_SECRET" + if [ -z "$JD_REGION" ]; then + _debug "Using default region: $_JD_DEFAULT_REGION" + JD_REGION="$_JD_DEFAULT_REGION" + else + _saveaccountconf_mutable JD_REGION "$JD_REGION" + fi + _JD_BASE_URI="$_JD_API_VERSION/regions/$JD_REGION" + + _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 getViewTree" + + _debug "Adding records" + + _addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1},\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}" + #_addrr='{"req":{"hostRecord":"xx","hostValue":"\"value4\"","jcloudRes":false,"mxPriority":null,"port":null,"ttl":300,"type":"TXT","weight":null,"viewValue":-1},"regionId":"cn-north-1","domainId":"8824"}' + if jd_rest POST "domain/$_domain_id/RRAdd" "" "$_addrr"; then + _rid="$(echo "$response" | tr '{},' '\n' | grep '"id":' | cut -d : -f 2)" + if [ -z "$_rid" ]; then + _err "Can not find record id from the result." + return 1 + fi + _info "TXT record added successfully." + _srid="$(_readdomainconf "JD_CLOUD_RIDS")" + if [ "$_srid" ]; then + _rid="$_srid,$_rid" + fi + _savedomainconf "JD_CLOUD_RIDS" "$_rid" + return 0 + fi + + return 1 +} + +dns_jd_rm() { + fulldomain=$1 + txtvalue=$2 + + JD_ACCESS_KEY_ID="${JD_ACCESS_KEY_ID:-$(_readaccountconf_mutable JD_ACCESS_KEY_ID)}" + JD_ACCESS_KEY_SECRET="${JD_ACCESS_KEY_SECRET:-$(_readaccountconf_mutable JD_ACCESS_KEY_SECRET)}" + JD_REGION="${JD_REGION:-$(_readaccountconf_mutable JD_REGION)}" + + if [ -z "$JD_REGION" ]; then + _debug "Using default region: $_JD_DEFAULT_REGION" + JD_REGION="$_JD_DEFAULT_REGION" + fi + + _JD_BASE_URI="$_JD_API_VERSION/regions/$JD_REGION" + + _info "Getting existing records for $fulldomain" + _srid="$(_readdomainconf "JD_CLOUD_RIDS")" + _debug _srid "$_srid" + + if [ -z "$_srid" ]; then + _err "Not rid skip" + return 0 + fi + + _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" + + _cleardomainconf JD_CLOUD_RIDS + + _aws_tmpl_xml="{\"ids\":[$_srid],\"action\":\"del\",\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}" + + if jd_rest POST "domain/$_domain_id/RROperate" "" "$_aws_tmpl_xml" && _contains "$response" "\"code\":\"OK\""; then + _info "TXT record deleted successfully." + return 0 + fi + return 1 + +} + +#################### Private functions below ################################## + +_get_root() { + domain=$1 + i=1 + p=1 + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug2 "Checking domain: $h" + if ! jd_rest GET "domain"; then + _err "error get domain list" + return 1 + fi + if [ -z "$h" ]; then + #not valid + _err "Invalid domain" + return 1 + fi + + if _contains "$response" "\"domainName\":\"$h\""; then + hostedzone="$(echo "$response" | tr '{}' '\n' | grep "\"domainName\":\"$h\"")" + _debug hostedzone "$hostedzone" + if [ "$hostedzone" ]; then + _domain_id="$(echo "$hostedzone" | tr ',' '\n' | grep "\"id\":" | cut -d : -f 2)" + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + fi + _err "Can't find domain with id: $h" + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + + return 1 +} + +#method uri qstr data +jd_rest() { + mtd="$1" + ep="$2" + qsr="$3" + data="$4" + + _debug mtd "$mtd" + _debug ep "$ep" + _debug qsr "$qsr" + _debug data "$data" + + CanonicalURI="/$_JD_BASE_URI/$ep" + _debug2 CanonicalURI "$CanonicalURI" + + CanonicalQueryString="$qsr" + _debug2 CanonicalQueryString "$CanonicalQueryString" + + RequestDate="$(date -u +"%Y%m%dT%H%M%SZ")" + #RequestDate="20190713T082155Z" ###################################################### + _debug2 RequestDate "$RequestDate" + export _H1="X-Jdcloud-Date: $RequestDate" + + RequestNonce="2bd0852a-8bae-4087-b2d5-$(_time)" + #RequestNonce="894baff5-72d4-4244-883a-7b2eb51e7fbe" ################################# + _debug2 RequestNonce "$RequestNonce" + export _H2="X-Jdcloud-Nonce: $RequestNonce" + + if [ "$data" ]; then + CanonicalHeaders="content-type:application/json\n" + SignedHeaders="content-type;" + else + CanonicalHeaders="" + SignedHeaders="" + fi + CanonicalHeaders="${CanonicalHeaders}host:$_JD_HOST\nx-jdcloud-date:$RequestDate\nx-jdcloud-nonce:$RequestNonce\n" + SignedHeaders="${SignedHeaders}host;x-jdcloud-date;x-jdcloud-nonce" + + _debug2 CanonicalHeaders "$CanonicalHeaders" + _debug2 SignedHeaders "$SignedHeaders" + + Hash="sha256" + + RequestPayload="$data" + _debug2 RequestPayload "$RequestPayload" + + RequestPayloadHash="$(printf "%s" "$RequestPayload" | _digest "$Hash" hex | _lower_case)" + _debug2 RequestPayloadHash "$RequestPayloadHash" + + CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$RequestPayloadHash" + _debug2 CanonicalRequest "$CanonicalRequest" + + HashedCanonicalRequest="$(printf "$CanonicalRequest%s" | _digest "$Hash" hex)" + _debug2 HashedCanonicalRequest "$HashedCanonicalRequest" + + Algorithm="JDCLOUD2-HMAC-SHA256" + _debug2 Algorithm "$Algorithm" + + RequestDateOnly="$(echo "$RequestDate" | cut -c 1-8)" + _debug2 RequestDateOnly "$RequestDateOnly" + + Region="$JD_REGION" + Service="$_JD_PROD" + + CredentialScope="$RequestDateOnly/$Region/$Service/jdcloud2_request" + _debug2 CredentialScope "$CredentialScope" + + StringToSign="$Algorithm\n$RequestDate\n$CredentialScope\n$HashedCanonicalRequest" + + _debug2 StringToSign "$StringToSign" + + kSecret="JDCLOUD2$JD_ACCESS_KEY_SECRET" + + _secure_debug2 kSecret "$kSecret" + + kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")" + _secure_debug2 kSecretH "$kSecretH" + + kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)" + _debug2 kDateH "$kDateH" + + kRegionH="$(printf "$Region%s" | _hmac "$Hash" "$kDateH" hex)" + _debug2 kRegionH "$kRegionH" + + kServiceH="$(printf "$Service%s" | _hmac "$Hash" "$kRegionH" hex)" + _debug2 kServiceH "$kServiceH" + + kSigningH="$(printf "%s" "jdcloud2_request" | _hmac "$Hash" "$kServiceH" hex)" + _debug2 kSigningH "$kSigningH" + + signature="$(printf "$StringToSign%s" | _hmac "$Hash" "$kSigningH" hex)" + _debug2 signature "$signature" + + Authorization="$Algorithm Credential=$JD_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature" + _debug2 Authorization "$Authorization" + + _H3="Authorization: $Authorization" + _debug _H3 "$_H3" + + url="https://$_JD_HOST$CanonicalURI" + if [ "$qsr" ]; then + url="https://$_JD_HOST$CanonicalURI?$qsr" + fi + + if [ "$mtd" = "GET" ]; then + response="$(_get "$url")" + else + response="$(_post "$data" "$url" "" "$mtd" "application/json")" + fi + + _ret="$?" + _debug2 response "$response" + if [ "$_ret" = "0" ]; then + if _contains "$response" "\"error\""; then + _err "Response error:$response" + return 1 + fi + fi + + return "$_ret" +} diff --git a/dnsapi/dns_joker.sh b/dnsapi/dns_joker.sh new file mode 100644 index 00000000..78399a1d --- /dev/null +++ b/dnsapi/dns_joker.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env sh + +# Joker.com API for acme.sh +# +# This script adds the necessary TXT record to a domain in Joker.com. +# +# You must activate Dynamic DNS in Joker.com DNS configuration first. +# Username and password below refer to Dynamic DNS authentication, +# not your Joker.com login credentials. +# See: https://joker.com/faq/content/11/427/en/what-is-dynamic-dns-dyndns.html +# +# NOTE: This script does not support wildcard certificates, because +# Joker.com API does not support adding two TXT records with the same +# subdomain. Adding the second record will overwrite the first one. +# See: https://joker.com/faq/content/6/496/en/let_s-encrypt-support.html +# "... this request will replace all TXT records for the specified +# label by the provided content" +# +# Author: aattww (https://github.com/aattww/) +# +# Report bugs to https://github.com/acmesh-official/acme.sh/issues/2840 +# +# JOKER_USERNAME="xxxx" +# JOKER_PASSWORD="xxxx" + +JOKER_API="https://svc.joker.com/nic/replace" + +######## Public functions ##################### + +#Usage: dns_joker_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_joker_add() { + fulldomain=$1 + txtvalue=$2 + + JOKER_USERNAME="${JOKER_USERNAME:-$(_readaccountconf_mutable JOKER_USERNAME)}" + JOKER_PASSWORD="${JOKER_PASSWORD:-$(_readaccountconf_mutable JOKER_PASSWORD)}" + + if [ -z "$JOKER_USERNAME" ] || [ -z "$JOKER_PASSWORD" ]; then + _err "No Joker.com username and password specified." + return 1 + fi + + _saveaccountconf_mutable JOKER_USERNAME "$JOKER_USERNAME" + _saveaccountconf_mutable JOKER_PASSWORD "$JOKER_PASSWORD" + + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + _info "Adding TXT record" + if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value=$txtvalue"; then + if _startswith "$response" "OK"; then + _info "Added, OK" + return 0 + fi + fi + _err "Error adding TXT record." + return 1 +} + +#fulldomain txtvalue +dns_joker_rm() { + fulldomain=$1 + txtvalue=$2 + + JOKER_USERNAME="${JOKER_USERNAME:-$(_readaccountconf_mutable JOKER_USERNAME)}" + JOKER_PASSWORD="${JOKER_PASSWORD:-$(_readaccountconf_mutable JOKER_PASSWORD)}" + + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + _info "Removing TXT record" + # TXT record is removed by setting its value to empty. + if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value="; then + if _startswith "$response" "OK"; then + _info "Removed, OK" + return 0 + fi + fi + _err "Error removing TXT record." + return 1 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + fulldomain=$1 + i=1 + while true; do + h=$(printf "%s" "$fulldomain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + return 1 + fi + + # Try to remove a test record. With correct root domain, username and password this will return "OK: ..." regardless + # of record in question existing or not. + if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$h&label=jokerTXTUpdateTest&type=TXT&value="; then + if _startswith "$response" "OK"; then + _sub_domain="$(echo "$fulldomain" | sed "s/\\.$h\$//")" + _domain=$h + return 0 + fi + fi + + i=$(_math "$i" + 1) + done + + _debug "Root domain not found" + return 1 +} + +_joker_rest() { + data="$1" + _debug data "$data" + + if ! response="$(_post "$data" "$JOKER_API" "" "POST")"; then + _err "Error POSTing" + return 1 + fi + _debug response "$response" + return 0 +} diff --git a/dnsapi/dns_kappernet.sh b/dnsapi/dns_kappernet.sh new file mode 100644 index 00000000..0a8951cb --- /dev/null +++ b/dnsapi/dns_kappernet.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env sh + +# kapper.net domain api +# for further questions please contact: support@kapper.net +# please report issues here: https://github.com/acmesh-official/acme.sh/issues/2977 + +#KAPPERNETDNS_Key="yourKAPPERNETapikey" +#KAPPERNETDNS_Secret="yourKAPPERNETapisecret" + +KAPPERNETDNS_Api="https://dnspanel.kapper.net/API/1.2?APIKey=$KAPPERNETDNS_Key&APISecret=$KAPPERNETDNS_Secret" + +############################################################################### +# called with +# fullhostname: something.example.com +# txtvalue: someacmegenerated string +dns_kappernet_add() { + fullhostname=$1 + txtvalue=$2 + + KAPPERNETDNS_Key="${KAPPERNETDNS_Key:-$(_readaccountconf_mutable KAPPERNETDNS_Key)}" + KAPPERNETDNS_Secret="${KAPPERNETDNS_Secret:-$(_readaccountconf_mutable KAPPERNETDNS_Secret)}" + + if [ -z "$KAPPERNETDNS_Key" ] || [ -z "$KAPPERNETDNS_Secret" ]; then + KAPPERNETDNS_Key="" + KAPPERNETDNS_Secret="" + _err "Please specify your kapper.net api key and secret." + _err "If you have not received yours - send your mail to" + _err "support@kapper.net to get your key and secret." + return 1 + fi + + #store the api key and email to the account conf file. + _saveaccountconf_mutable KAPPERNETDNS_Key "$KAPPERNETDNS_Key" + _saveaccountconf_mutable KAPPERNETDNS_Secret "$KAPPERNETDNS_Secret" + _debug "Checking Domain ..." + if ! _get_root "$fullhostname"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "SUBDOMAIN: $_sub_domain" + _debug _domain "DOMAIN: $_domain" + + _info "Trying to add TXT DNS Record" + data="%7B%22name%22%3A%22$fullhostname%22%2C%22type%22%3A%22TXT%22%2C%22content%22%3A%22$txtvalue%22%2C%22ttl%22%3A%223600%22%2C%22prio%22%3A%22%22%7D" + if _kappernet_api GET "action=new&subject=$_domain&data=$data"; then + + if _contains "$response" "{\"OK\":true"; then + _info "Waiting 1 second for DNS to spread the new record" + _sleep 1 + return 0 + else + _err "Error creating a TXT DNS Record: $fullhostname TXT $txtvalue" + _err "Error Message: $response" + return 1 + fi + fi + _err "Failed creating TXT Record" +} + +############################################################################### +# called with +# fullhostname: something.example.com +dns_kappernet_rm() { + fullhostname=$1 + txtvalue=$2 + + KAPPERNETDNS_Key="${KAPPERNETDNS_Key:-$(_readaccountconf_mutable KAPPERNETDNS_Key)}" + KAPPERNETDNS_Secret="${KAPPERNETDNS_Secret:-$(_readaccountconf_mutable KAPPERNETDNS_Secret)}" + + if [ -z "$KAPPERNETDNS_Key" ] || [ -z "$KAPPERNETDNS_Secret" ]; then + KAPPERNETDNS_Key="" + KAPPERNETDNS_Secret="" + _err "Please specify your kapper.net api key and secret." + _err "If you have not received yours - send your mail to" + _err "support@kapper.net to get your key and secret." + return 1 + fi + + #store the api key and email to the account conf file. + _saveaccountconf_mutable KAPPERNETDNS_Key "$KAPPERNETDNS_Key" + _saveaccountconf_mutable KAPPERNETDNS_Secret "$KAPPERNETDNS_Secret" + + _info "Trying to remove the TXT Record: $fullhostname containing $txtvalue" + data="%7B%22name%22%3A%22$fullhostname%22%2C%22type%22%3A%22TXT%22%2C%22content%22%3A%22$txtvalue%22%2C%22ttl%22%3A%223600%22%2C%22prio%22%3A%22%22%7D" + if _kappernet_api GET "action=del&subject=$fullhostname&data=$data"; then + if _contains "$response" "{\"OK\":true"; then + return 0 + else + _err "Error deleting DNS Record: $fullhostname containing $txtvalue" + _err "Problem: $response" + return 1 + fi + fi + _err "Problem deleting TXT DNS record" +} + +#################### Private functions below ################################## +# called with hostname +# e.g._acme-challenge.www.domain.com returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + if ! _kappernet_api GET "action=list&subject=$h"; then + return 1 + fi + if _contains "$response" '"OK":false'; then + _debug "$h not found" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +################################################################################ +# calls the kapper.net DNS Panel API +# with +# method +# param +_kappernet_api() { + method=$1 + param="$2" + + _debug param "PARAMETER=$param" + url="$KAPPERNETDNS_Api&$param" + _debug url "URL=$url" + + if [ "$method" = "GET" ]; then + response="$(_get "$url")" + else + _err "Unsupported method" + return 1 + fi + + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_kas.sh b/dnsapi/dns_kas.sh new file mode 100755 index 00000000..1253cf27 --- /dev/null +++ b/dnsapi/dns_kas.sh @@ -0,0 +1,281 @@ +#!/usr/bin/env sh +######################################################################## +# All-inkl Kasserver hook script for acme.sh +# +# Environment variables: +# +# - $KAS_Login (Kasserver API login name) +# - $KAS_Authtype (Kasserver API auth type. Default: plain) +# - $KAS_Authdata (Kasserver API auth data.) +# +# Last update: squared GmbH +# Credits: +# - dns_he.sh. Thanks a lot man! +# - Martin Kammerlander, Phlegx Systems OG +# - Marc-Oliver Lange +# - https://github.com/o1oo11oo/kasapi.sh +######################################################################## +KAS_Api_GET="$(_get "https://kasapi.kasserver.com/soap/wsdl/KasApi.wsdl")" +KAS_Api="$(echo "$KAS_Api_GET" | tr -d ' ' | grep -i "//g")" +_info "[KAS] -> API URL $KAS_Api" + +KAS_Auth_GET="$(_get "https://kasapi.kasserver.com/soap/wsdl/KasAuth.wsdl")" +KAS_Auth="$(echo "$KAS_Auth_GET" | tr -d ' ' | grep -i "//g")" +_info "[KAS] -> AUTH URL $KAS_Auth" + +KAS_default_ratelimit=5 # TODO - Every response delivers a ratelimit (seconds) where KASAPI is blocking a request. + +######## Public functions ##################### +dns_kas_add() { + _fulldomain=$1 + _txtvalue=$2 + + _info "[KAS] -> Using DNS-01 All-inkl/Kasserver hook" + _info "[KAS] -> Check and Save Props" + _check_and_save + + _info "[KAS] -> Adding $_fulldomain DNS TXT entry on all-inkl.com/Kasserver" + _info "[KAS] -> Retriving Credential Token" + _get_credential_token + + _info "[KAS] -> Checking Zone and Record_Name" + _get_zone_and_record_name "$_fulldomain" + + _info "[KAS] -> Checking for existing Record entries" + _get_record_id + + # If there is a record_id, delete the entry + if [ -n "$_record_id" ]; then + _info "[KAS] -> Existing records found. Now deleting old entries" + for i in $_record_id; do + _delete_RecordByID "$i" + done + else + _info "[KAS] -> No record found." + fi + + _info "[KAS] -> Creating TXT DNS record" + action="add_dns_settings" + kasReqParam="\"record_name\":\"$_record_name\"" + kasReqParam="$kasReqParam,\"record_type\":\"TXT\"" + kasReqParam="$kasReqParam,\"record_data\":\"$_txtvalue\"" + kasReqParam="$kasReqParam,\"record_aux\":\"0\"" + kasReqParam="$kasReqParam,\"zone_host\":\"$_zone\"" + response="$(_callAPI "$action" "$kasReqParam")" + _debug2 "[KAS] -> Response" "$response" + + if [ -z "$response" ]; then + _info "[KAS] -> Response was empty, please check manually." + return 1 + elif _contains "$response" ""; then + faultstring="$(echo "$response" | tr -d '\n\r' | sed "s//\n=> /g" | sed "s/<\/faultstring>/\n/g" | grep "=>" | sed "s/=> //g")" + case "${faultstring}" in + "record_already_exists") + _info "[KAS] -> The record already exists, which must not be a problem. Please check manually." + ;; + *) + _err "[KAS] -> An error =>$faultstring<= occurred, please check manually." + return 1 + ;; + esac + elif ! _contains "$response" "ReturnStringTRUE"; then + _err "[KAS] -> An unknown error occurred, please check manually." + return 1 + fi + return 0 +} + +dns_kas_rm() { + _fulldomain=$1 + _txtvalue=$2 + + _info "[KAS] -> Using DNS-01 All-inkl/Kasserver hook" + _info "[KAS] -> Check and Save Props" + _check_and_save + + _info "[KAS] -> Cleaning up after All-inkl/Kasserver hook" + _info "[KAS] -> Removing $_fulldomain DNS TXT entry on All-inkl/Kasserver" + _info "[KAS] -> Retriving Credential Token" + _get_credential_token + + _info "[KAS] -> Checking Zone and Record_Name" + _get_zone_and_record_name "$_fulldomain" + + _info "[KAS] -> Getting Record ID" + _get_record_id + + _info "[KAS] -> Removing entries with ID: $_record_id" + # If there is a record_id, delete the entry + if [ -n "$_record_id" ]; then + for i in $_record_id; do + _delete_RecordByID "$i" + done + else # Cannot delete or unkown error + _info "[KAS] -> No record_id found that can be deleted. Please check manually." + fi + return 0 +} + +########################## PRIVATE FUNCTIONS ########################### +# Delete Record ID +_delete_RecordByID() { + recId=$1 + action="delete_dns_settings" + kasReqParam="\"record_id\":\"$recId\"" + response="$(_callAPI "$action" "$kasReqParam")" + _debug2 "[KAS] -> Response" "$response" + + if [ -z "$response" ]; then + _info "[KAS] -> Response was empty, please check manually." + return 1 + elif _contains "$response" ""; then + faultstring="$(echo "$response" | tr -d '\n\r' | sed "s//\n=> /g" | sed "s/<\/faultstring>/\n/g" | grep "=>" | sed "s/=> //g")" + case "${faultstring}" in + "record_id_not_found") + _info "[KAS] -> The record was not found, which perhaps is not a problem. Please check manually." + ;; + *) + _err "[KAS] -> An error =>$faultstring<= occurred, please check manually." + return 1 + ;; + esac + elif ! _contains "$response" "ReturnStringTRUE"; then + _err "[KAS] -> An unknown error occurred, please check manually." + return 1 + fi +} +# Checks for the ENV variables and saves them +_check_and_save() { + KAS_Login="${KAS_Login:-$(_readaccountconf_mutable KAS_Login)}" + KAS_Authtype="${KAS_Authtype:-$(_readaccountconf_mutable KAS_Authtype)}" + KAS_Authdata="${KAS_Authdata:-$(_readaccountconf_mutable KAS_Authdata)}" + + if [ -z "$KAS_Login" ] || [ -z "$KAS_Authtype" ] || [ -z "$KAS_Authdata" ]; then + KAS_Login= + KAS_Authtype= + KAS_Authdata= + _err "[KAS] -> No auth details provided. Please set user credentials using the \$KAS_Login, \$KAS_Authtype, and \$KAS_Authdata environment variables." + return 1 + fi + _saveaccountconf_mutable KAS_Login "$KAS_Login" + _saveaccountconf_mutable KAS_Authtype "$KAS_Authtype" + _saveaccountconf_mutable KAS_Authdata "$KAS_Authdata" + return 0 +} + +# Gets back the base domain/zone and record name. +# See: https://github.com/Neilpang/acme.sh/wiki/DNS-API-Dev-Guide +_get_zone_and_record_name() { + action="get_domains" + response="$(_callAPI "$action")" + _debug2 "[KAS] -> Response" "$response" + + if [ -z "$response" ]; then + _info "[KAS] -> Response was empty, please check manually." + return 1 + elif _contains "$response" ""; then + faultstring="$(echo "$response" | tr -d '\n\r' | sed "s//\n=> /g" | sed "s/<\/faultstring>/\n/g" | grep "=>" | sed "s/=> //g")" + _err "[KAS] -> Either no domains were found or another error =>$faultstring<= occurred, please check manually." + return 1 + fi + + zonen="$(echo "$response" | sed 's//\n/g' | sed -r 's/(.*domain_name<\/key>)(.*)(<\/value.*)/\2/' | sed '/^ Zone:" "$_zone" + _debug "[KAS] -> Domain:" "$domain" + _debug "[KAS] -> Record_Name:" "$_record_name" + return 0 +} + +# Retrieve the DNS record ID +_get_record_id() { + action="get_dns_settings" + kasReqParam="\"zone_host\":\"$_zone\"" + response="$(_callAPI "$action" "$kasReqParam")" + _debug2 "[KAS] -> Response" "$response" + + if [ -z "$response" ]; then + _info "[KAS] -> Response was empty, please check manually." + return 1 + elif _contains "$response" ""; then + faultstring="$(echo "$response" | tr -d '\n\r' | sed "s//\n=> /g" | sed "s/<\/faultstring>/\n/g" | grep "=>" | sed "s/=> //g")" + _err "[KAS] -> Either no domains were found or another error =>$faultstring<= occurred, please check manually." + return 1 + fi + + _record_id="$(echo "$response" | tr -d '\n\r' | sed "s//\n/g" | grep -i "$_record_name" | grep -i ">TXT<" | sed "s/record_id<\/key>/=>/g" | grep -i "$_txtvalue" | sed "s/<\/value><\/item>/\n/g" | grep "=>" | sed "s/=>//g")" + _debug "[KAS] -> Record Id: " "$_record_id" + return 0 +} + +# Retrieve credential token +_get_credential_token() { + baseParamAuth="\"kas_login\":\"$KAS_Login\"" + baseParamAuth="$baseParamAuth,\"kas_auth_type\":\"$KAS_Authtype\"" + baseParamAuth="$baseParamAuth,\"kas_auth_data\":\"$KAS_Authdata\"" + baseParamAuth="$baseParamAuth,\"session_lifetime\":600" + baseParamAuth="$baseParamAuth,\"session_update_lifetime\":\"Y\"" + + data='{' + data="$data$baseParamAuth}" + + _debug "[KAS] -> Be friendly and wait $KAS_default_ratelimit seconds by default before calling KAS API." + _sleep $KAS_default_ratelimit + + contentType="text/xml" + export _H1="SOAPAction: urn:xmethodsKasApiAuthentication#KasAuth" + response="$(_post "$data" "$KAS_Auth" "" "POST" "$contentType")" + _debug2 "[KAS] -> Response" "$response" + + if [ -z "$response" ]; then + _info "[KAS] -> Response was empty, please check manually." + return 1 + elif _contains "$response" ""; then + faultstring="$(echo "$response" | tr -d '\n\r' | sed "s//\n=> /g" | sed "s/<\/faultstring>/\n/g" | grep "=>" | sed "s/=> //g")" + _err "[KAS] -> Could not retrieve login token or antoher error =>$faultstring<= occurred, please check manually." + return 1 + fi + + _credential_token="$(echo "$response" | tr '\n' ' ' | sed 's/.*return xsi:type="xsd:string">\(.*\)<\/return>/\1/' | sed 's/<\/ns1:KasAuthResponse\(.*\)Envelope>.*//')" + _debug "[KAS] -> Credential Token: " "$_credential_token" + return 0 +} + +_callAPI() { + kasaction=$1 + kasReqParams=$2 + + baseParamAuth="\"kas_login\":\"$KAS_Login\"" + baseParamAuth="$baseParamAuth,\"kas_auth_type\":\"session\"" + baseParamAuth="$baseParamAuth,\"kas_auth_data\":\"$_credential_token\"" + + data='{' + data="$data$baseParamAuth,\"kas_action\":\"$kasaction\"" + if [ -n "$kasReqParams" ]; then + data="$data,\"KasRequestParams\":{$kasReqParams}" + fi + data="$data}" + + _debug2 "[KAS] -> Request" "$data" + + _debug "[KAS] -> Be friendly and wait $KAS_default_ratelimit seconds by default before calling KAS API." + _sleep $KAS_default_ratelimit + + contentType="text/xml" + export _H1="SOAPAction: urn:xmethodsKasApi#KasApi" + response="$(_post "$data" "$KAS_Api" "" "POST" "$contentType")" + _debug2 "[KAS] -> Response" "$response" + echo "$response" +} diff --git a/dnsapi/dns_kinghost.sh b/dnsapi/dns_kinghost.sh index 898ab286..f640242f 100644 --- a/dnsapi/dns_kinghost.sh +++ b/dnsapi/dns_kinghost.sh @@ -2,7 +2,7 @@ ############################################################ # KingHost API support # -# http://api.kinghost.net/doc/ # +# https://api.kinghost.net/doc/ # # # # Author: Felipe Keller Braz # # Report Bugs here: https://github.com/kinghost/acme.sh # @@ -37,7 +37,7 @@ dns_kinghost_add() { _debug "Getting txt records" _kinghost_rest GET "dns" "name=$fulldomain&content=$txtvalue" - #This API call returns "status":"ok" if dns record does not exists + #This API call returns "status":"ok" if dns record does not exist #We are creating a new txt record here, so we expect the "ok" status if ! echo "$response" | grep '"status":"ok"' >/dev/null; then _err "Error" diff --git a/dnsapi/dns_knot.sh b/dnsapi/dns_knot.sh index 094a6981..729a89cb 100644 --- a/dnsapi/dns_knot.sh +++ b/dnsapi/dns_knot.sh @@ -19,8 +19,9 @@ dns_knot_add() { _info "Adding ${fulldomain}. 60 TXT \"${txtvalue}\"" - knsupdate -y "${KNOT_KEY}" < +#Utilize leaseweb.com API to finish dns-01 verifications. +#Requires a Leaseweb API Key (export LSW_Key="Your Key") +#See https://developer.leaseweb.com for more information. +######## Public functions ##################### + +LSW_API="https://api.leaseweb.com/hosting/v2/domains" + +#Usage: dns_leaseweb_add _acme-challenge.www.domain.com +dns_leaseweb_add() { + fulldomain=$1 + txtvalue=$2 + + LSW_Key="${LSW_Key:-$(_readaccountconf_mutable LSW_Key)}" + if [ -z "$LSW_Key" ]; then + LSW_Key="" + _err "You don't specify Leaseweb api key yet." + _err "Please create your key and try again." + return 1 + fi + + #save the api key to the account conf file. + _saveaccountconf_mutable LSW_Key "$LSW_Key" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _root_domain "$_domain" + _debug _domain "$fulldomain" + + if _lsw_api "POST" "$_domain" "$fulldomain" "$txtvalue"; then + if [ "$_code" = "201" ]; then + _info "Added, OK" + return 0 + else + _err "Add txt record error, invalid code. Code: $_code" + return 1 + fi + fi + _err "Add txt record error." + + return 1 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_leaseweb_rm() { + fulldomain=$1 + txtvalue=$2 + + LSW_Key="${LSW_Key:-$(_readaccountconf_mutable LSW_Key)}" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _root_domain "$_domain" + _debug _domain "$fulldomain" + + if _lsw_api "DELETE" "$_domain" "$fulldomain" "$txtvalue"; then + if [ "$_code" = "204" ]; then + _info "Deleted, OK" + return 0 + else + _err "Delete txt record error." + return 1 + fi + fi + _err "Delete txt record error." + + return 1 +} + +#################### Private functions below ################################## +# _acme-challenge.www.domain.com +# returns +# _domain=domain.com +_get_root() { + rdomain=$1 + i="$(echo "$rdomain" | tr '.' ' ' | wc -w)" + i=$(_math "$i" - 1) + + while true; do + h=$(printf "%s" "$rdomain" | cut -d . -f "$i"-100) + _debug h "$h" + if [ -z "$h" ]; then + return 1 #not valid domain + fi + + #Check API if domain exists + if _lsw_api "GET" "$h"; then + if [ "$_code" = "200" ]; then + _domain="$h" + return 0 + fi + fi + i=$(_math "$i" - 1) + if [ "$i" -lt 2 ]; then + return 1 #not found, no need to check _acme-challenge.sub.domain in leaseweb api. + fi + done + + return 1 +} + +_lsw_api() { + cmd=$1 + d=$2 + fd=$3 + tvalue=$4 + + # Construct the HTTP Authorization header + export _H2="Content-Type: application/json" + export _H1="X-Lsw-Auth: ${LSW_Key}" + + if [ "$cmd" = "GET" ]; then + response="$(_get "$LSW_API/$d")" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug "http response code $_code" + _debug response "$response" + return 0 + fi + + if [ "$cmd" = "POST" ]; then + data="{\"name\": \"$fd.\",\"type\": \"TXT\",\"content\": [\"$tvalue\"],\"ttl\": 60}" + response="$(_post "$data" "$LSW_API/$d/resourceRecordSets" "$data" "POST")" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug "http response code $_code" + _debug response "$response" + return 0 + fi + + if [ "$cmd" = "DELETE" ]; then + response="$(_post "" "$LSW_API/$d/resourceRecordSets/$fd/TXT" "" "DELETE")" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug "http response code $_code" + _debug response "$response" + return 0 + fi + + return 1 +} diff --git a/dnsapi/dns_lexicon.sh b/dnsapi/dns_lexicon.sh index f6f54464..19702343 100755 --- a/dnsapi/dns_lexicon.sh +++ b/dnsapi/dns_lexicon.sh @@ -5,7 +5,7 @@ # https://github.com/AnalogJ/lexicon lexicon_cmd="lexicon" -wiki="https://github.com/Neilpang/acme.sh/wiki/How-to-use-lexicon-dns-api" +wiki="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-lexicon-dns-api" _lexicon_init() { if ! _exists "$lexicon_cmd"; then @@ -63,6 +63,16 @@ _lexicon_init() { _saveaccountconf_mutable "$Lx_domaintoken" "$Lx_domaintoken_v" eval export "$Lx_domaintoken" fi + + # shellcheck disable=SC2018,SC2019 + Lx_api_key=$(echo LEXICON_"${PROVIDER}"_API_KEY | tr 'a-z' 'A-Z') + eval "$Lx_api_key=\${$Lx_api_key:-$(_readaccountconf_mutable "$Lx_api_key")}" + Lx_api_key_v=$(eval echo \$"$Lx_api_key") + _secure_debug "$Lx_api_key" "$Lx_api_key_v" + if [ "$Lx_api_key_v" ]; then + _saveaccountconf_mutable "$Lx_api_key" "$Lx_api_key_v" + eval export "$Lx_api_key" + fi } ######## Public functions ##################### @@ -82,7 +92,7 @@ dns_lexicon_add() { _savedomainconf LEXICON_OPTS "$LEXICON_OPTS" # shellcheck disable=SC2086 - $lexicon_cmd "$PROVIDER" $LEXICON_OPTS create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}" + $lexicon_cmd "$PROVIDER" $LEXICON_OPTS create "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}" --output QUIET } @@ -98,6 +108,6 @@ dns_lexicon_rm() { domain=$(printf "%s" "$fulldomain" | cut -d . -f 2-999) # shellcheck disable=SC2086 - $lexicon_cmd "$PROVIDER" $LEXICON_OPTS delete "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}" + $lexicon_cmd "$PROVIDER" $LEXICON_OPTS delete "${domain}" TXT --name="_acme-challenge.${domain}." --content="${txtvalue}" --output QUIET } diff --git a/dnsapi/dns_linode_v4.sh b/dnsapi/dns_linode_v4.sh new file mode 100755 index 00000000..9504afbf --- /dev/null +++ b/dnsapi/dns_linode_v4.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env sh + +#Original Author: Philipp Grosswiler +#v4 Update Author: Aaron W. Swenson + +LINODE_V4_API_URL="https://api.linode.com/v4/domains" + +######## Public functions ##################### + +#Usage: dns_linode_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_linode_v4_add() { + fulldomain="${1}" + txtvalue="${2}" + + if ! _Linode_API; then + return 1 + fi + + _info "Using Linode" + _debug "Calling: dns_linode_add() '${fulldomain}' '${txtvalue}'" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Domain does not exist." + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _payload="{ + \"type\": \"TXT\", + \"name\": \"$_sub_domain\", + \"target\": \"$txtvalue\", + \"ttl_sec\": 300 + }" + + if _rest POST "/$_domain_id/records" "$_payload" && [ -n "$response" ]; then + _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"id\": *[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1) + _debug _resource_id "$_resource_id" + + if [ -z "$_resource_id" ]; then + _err "Error adding the domain resource." + return 1 + fi + + _info "Domain resource successfully added." + return 0 + fi + + return 1 +} + +#Usage: dns_linode_rm _acme-challenge.www.domain.com +dns_linode_v4_rm() { + fulldomain="${1}" + + if ! _Linode_API; then + return 1 + fi + + _info "Using Linode" + _debug "Calling: dns_linode_rm() '${fulldomain}'" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Domain does not exist." + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + if _rest GET "/$_domain_id/records" && [ -n "$response" ]; then + response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")" + + resource="$(echo "$response" | _egrep_o "\{.*\"name\": *\"$_sub_domain\".*}")" + if [ "$resource" ]; then + _resource_id=$(printf "%s\n" "$resource" | _egrep_o "\"id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + if [ "$_resource_id" ]; then + _debug _resource_id "$_resource_id" + + if _rest DELETE "/$_domain_id/records/$_resource_id" && [ -n "$response" ]; then + # On 200/OK, empty set is returned. Check for error, if any. + _error_response=$(printf "%s\n" "$response" | _egrep_o "\"errors\"" | cut -d : -f 2 | tr -d " " | _head_n 1) + + if [ -n "$_error_response" ]; then + _err "Error deleting the domain resource: $_error_response" + return 1 + fi + + _info "Domain resource successfully deleted." + return 0 + fi + fi + + return 1 + fi + + return 0 + fi + + return 1 +} + +#################### Private functions below ################################## + +_Linode_API() { + LINODE_V4_API_KEY="${LINODE_V4_API_KEY:-$(_readaccountconf_mutable LINODE_V4_API_KEY)}" + if [ -z "$LINODE_V4_API_KEY" ]; then + LINODE_V4_API_KEY="" + + _err "You didn't specify the Linode v4 API key yet." + _err "Please create your key and try again." + + return 1 + fi + + _saveaccountconf_mutable LINODE_V4_API_KEY "$LINODE_V4_API_KEY" +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=12345 +_get_root() { + domain=$1 + i=2 + p=1 + + if _rest GET; then + response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")" + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + hostedzone="$(echo "$response" | _egrep_o "\{.*\"domain\": *\"$h\".*}")" + if [ "$hostedzone" ]; then + _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + fi + return 1 +} + +#method method action data +_rest() { + mtd="$1" + ep="$2" + data="$3" + + _debug mtd "$mtd" + _debug ep "$ep" + + export _H1="Accept: application/json" + export _H2="Content-Type: application/json" + export _H3="Authorization: Bearer $LINODE_V4_API_KEY" + + if [ "$mtd" != "GET" ]; then + # both POST and DELETE. + _debug data "$data" + response="$(_post "$data" "$LINODE_V4_API_URL$ep" "" "$mtd")" + else + response="$(_get "$LINODE_V4_API_URL$ep$data")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_loopia.sh b/dnsapi/dns_loopia.sh index 5d761187..60d072e0 100644 --- a/dnsapi/dns_loopia.sh +++ b/dnsapi/dns_loopia.sh @@ -4,8 +4,10 @@ #LOOPIA_User="username" # #LOOPIA_Password="password" +# +#LOOPIA_Api="https://api.loopia./RPCSERV" -LOOPIA_Api="https://api.loopia.se/RPCSERV" +LOOPIA_Api_Default="https://api.loopia.se/RPCSERV" ######## Public functions ##################### @@ -14,19 +16,11 @@ dns_loopia_add() { fulldomain=$1 txtvalue=$2 - LOOPIA_User="${LOOPIA_User:-$(_readaccountconf_mutable LOOPIA_User)}" - LOOPIA_Password="${LOOPIA_Password:-$(_readaccountconf_mutable LOOPIA_Password)}" - if [ -z "$LOOPIA_User" ] || [ -z "$LOOPIA_Password" ]; then - LOOPIA_User="" - LOOPIA_Password="" - _err "You don't specify loopia user and password yet." - _err "Please create you key and try again." + if ! _loopia_load_config; then return 1 fi - #save the api key and email to the account conf file. - _saveaccountconf_mutable LOOPIA_User "$LOOPIA_User" - _saveaccountconf_mutable LOOPIA_Password "$LOOPIA_Password" + _loopia_save_config _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -38,8 +32,12 @@ dns_loopia_add() { _info "Adding record" - _loopia_add_record "$_domain" "$_sub_domain" - _loopia_update_record "$_domain" "$_sub_domain" "$txtvalue" + if ! _loopia_add_sub_domain "$_domain" "$_sub_domain"; then + return 1 + fi + if ! _loopia_add_record "$_domain" "$_sub_domain" "$txtvalue"; then + return 1 + fi } @@ -47,19 +45,11 @@ dns_loopia_rm() { fulldomain=$1 txtvalue=$2 - LOOPIA_User="${LOOPIA_User:-$(_readaccountconf_mutable LOOPIA_User)}" - LOOPIA_Password="${LOOPIA_Password:-$(_readaccountconf_mutable LOOPIA_Password)}" - if [ -z "$LOOPIA_User" ] || [ -z "$LOOPIA_Password" ]; then - LOOPIA_User="" - LOOPIA_Password="" - _err "You don't specify LOOPIA user and password yet." - _err "Please create you key and try again." + if ! _loopia_load_config; then return 1 fi - #save the api key and email to the account conf file. - _saveaccountconf_mutable LOOPIA_User "$LOOPIA_User" - _saveaccountconf_mutable LOOPIA_Password "$LOOPIA_Password" + _loopia_save_config _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -84,18 +74,87 @@ dns_loopia_rm() { %s - ' $LOOPIA_User $LOOPIA_Password "$_domain" "$_sub_domain") + ' "$LOOPIA_User" "$Encoded_Password" "$_domain" "$_sub_domain") response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" if ! _contains "$response" "OK"; then - _err "Error could not get txt records" + err_response=$(echo "$response" | sed 's/.*\(.*\)<\/string>.*/\1/') + _err "Error could not get txt records: $err_response" return 1 fi } #################### Private functions below ################################## +_loopia_load_config() { + LOOPIA_Api="${LOOPIA_Api:-$(_readaccountconf_mutable LOOPIA_Api)}" + LOOPIA_User="${LOOPIA_User:-$(_readaccountconf_mutable LOOPIA_User)}" + LOOPIA_Password="${LOOPIA_Password:-$(_readaccountconf_mutable LOOPIA_Password)}" + + if [ -z "$LOOPIA_Api" ]; then + LOOPIA_Api="$LOOPIA_Api_Default" + fi + + if [ -z "$LOOPIA_User" ] || [ -z "$LOOPIA_Password" ]; then + LOOPIA_User="" + LOOPIA_Password="" + + _err "A valid Loopia API user and password not provided." + _err "Please provide a valid API user and try again." + + return 1 + fi + + if _contains "$LOOPIA_Password" "'" || _contains "$LOOPIA_Password" '"'; then + _err "Password contains a quotation mark or double quotation marks and this is not supported by dns_loopia.sh" + return 1 + fi + + Encoded_Password=$(_xml_encode "$LOOPIA_Password") + return 0 +} + +_loopia_save_config() { + if [ "$LOOPIA_Api" != "$LOOPIA_Api_Default" ]; then + _saveaccountconf_mutable LOOPIA_Api "$LOOPIA_Api" + fi + _saveaccountconf_mutable LOOPIA_User "$LOOPIA_User" + _saveaccountconf_mutable LOOPIA_Password "$LOOPIA_Password" +} + +_loopia_get_records() { + domain=$1 + sub_domain=$2 + + xml_content=$(printf ' + + getZoneRecords + + + %s + + + %s + + + %s + + + %s + + + ' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain") + + response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" + if ! _contains "$response" ""; then + err_response=$(echo "$response" | sed 's/.*\(.*\)<\/string>.*/\1/') + _err "Error: $err_response" + return 1 + fi + return 0 +} + _get_root() { domain=$1 _debug "get root" @@ -115,7 +174,7 @@ _get_root() { %s - ' $LOOPIA_User $LOOPIA_Password) + ' "$LOOPIA_User" "$Encoded_Password") response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" while true; do @@ -137,14 +196,14 @@ _get_root() { } -_loopia_update_record() { +_loopia_add_record() { domain=$1 sub_domain=$2 txtval=$3 xml_content=$(printf ' - updateZoneRecord + addZoneRecord %s @@ -159,45 +218,76 @@ _loopia_update_record() { %s - - - type - TXT - - - priority - 0 - - - ttl - 60 - - - rdata - %s - - - record_id - 0 - - + + + + type + TXT + + + priority + 0 + + + ttl + 300 + + + rdata + %s + + + - ' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain" "$txtval") + ' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain" "$txtval") response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" if ! _contains "$response" "OK"; then - _err "Error" + err_response=$(echo "$response" | sed 's/.*\(.*\)<\/string>.*/\1/') + _err "Error: $err_response" return 1 fi return 0 } -_loopia_add_record() { +_sub_domain_exists() { domain=$1 sub_domain=$2 + xml_content=$(printf ' + + getSubdomains + + + %s + + + %s + + + %s + + + ' "$LOOPIA_User" "$Encoded_Password" "$domain") + + response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" + + if _contains "$response" "$sub_domain"; then + return 0 + fi + return 1 +} + +_loopia_add_sub_domain() { + domain=$1 + sub_domain=$2 + + if _sub_domain_exists "$domain" "$sub_domain"; then + return 0 + fi + xml_content=$(printf ' addSubdomain @@ -215,13 +305,22 @@ _loopia_add_record() { %s - ' $LOOPIA_User $LOOPIA_Password "$domain" "$sub_domain") + ' "$LOOPIA_User" "$Encoded_Password" "$domain" "$sub_domain") response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")" if ! _contains "$response" "OK"; then - _err "Error" + err_response=$(echo "$response" | sed 's/.*\(.*\)<\/string>.*/\1/') + _err "Error: $err_response" return 1 fi return 0 } + +_xml_encode() { + encoded_string=$1 + encoded_string=$(echo "$encoded_string" | sed 's/&/\&/') + encoded_string=$(echo "$encoded_string" | sed 's//\>/') + printf "%s" "$encoded_string" +} diff --git a/dnsapi/dns_maradns.sh b/dnsapi/dns_maradns.sh new file mode 100755 index 00000000..4ff6ca2d --- /dev/null +++ b/dnsapi/dns_maradns.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env sh + +#Usage: dns_maradns_add _acme-challenge.www.domain.com "token" +dns_maradns_add() { + fulldomain="$1" + txtvalue="$2" + + MARA_ZONE_FILE="${MARA_ZONE_FILE:-$(_readaccountconf_mutable MARA_ZONE_FILE)}" + MARA_DUENDE_PID_PATH="${MARA_DUENDE_PID_PATH:-$(_readaccountconf_mutable MARA_DUENDE_PID_PATH)}" + + _check_zone_file "$MARA_ZONE_FILE" || return 1 + _check_duende_pid_path "$MARA_DUENDE_PID_PATH" || return 1 + + _saveaccountconf_mutable MARA_ZONE_FILE "$MARA_ZONE_FILE" + _saveaccountconf_mutable MARA_DUENDE_PID_PATH "$MARA_DUENDE_PID_PATH" + + printf "%s. TXT '%s' ~\n" "$fulldomain" "$txtvalue" >>"$MARA_ZONE_FILE" + _reload_maradns "$MARA_DUENDE_PID_PATH" || return 1 +} + +#Usage: dns_maradns_rm _acme-challenge.www.domain.com "token" +dns_maradns_rm() { + fulldomain="$1" + txtvalue="$2" + + MARA_ZONE_FILE="${MARA_ZONE_FILE:-$(_readaccountconf_mutable MARA_ZONE_FILE)}" + MARA_DUENDE_PID_PATH="${MARA_DUENDE_PID_PATH:-$(_readaccountconf_mutable MARA_DUENDE_PID_PATH)}" + + _check_zone_file "$MARA_ZONE_FILE" || return 1 + _check_duende_pid_path "$MARA_DUENDE_PID_PATH" || return 1 + + _saveaccountconf_mutable MARA_ZONE_FILE "$MARA_ZONE_FILE" + _saveaccountconf_mutable MARA_DUENDE_PID_PATH "$MARA_DUENDE_PID_PATH" + + _sed_i "/^$fulldomain.\+TXT '$txtvalue' ~/d" "$MARA_ZONE_FILE" + _reload_maradns "$MARA_DUENDE_PID_PATH" || return 1 +} + +_check_zone_file() { + zonefile="$1" + if [ -z "$zonefile" ]; then + _err "MARA_ZONE_FILE not passed!" + return 1 + elif [ ! -w "$zonefile" ]; then + _err "MARA_ZONE_FILE not writable: $zonefile" + return 1 + fi +} + +_check_duende_pid_path() { + pidpath="$1" + if [ -z "$pidpath" ]; then + _err "MARA_DUENDE_PID_PATH not passed!" + return 1 + fi + if [ ! -r "$pidpath" ]; then + _err "MARA_DUENDE_PID_PATH not readable: $pidpath" + return 1 + fi +} + +_reload_maradns() { + pidpath="$1" + kill -s HUP -- "$(cat "$pidpath")" + if [ $? -ne 0 ]; then + _err "Unable to reload MaraDNS, kill returned $?" + return 1 + fi +} diff --git a/dnsapi/dns_me.sh b/dnsapi/dns_me.sh index 382eeedd..49007402 100644 --- a/dnsapi/dns_me.sh +++ b/dnsapi/dns_me.sh @@ -2,7 +2,7 @@ # bug reports to dev@1e.ca -# ME_Key=qmlkdjflmkqdjf +# ME_Key=qmlkdjflmkqdjf # ME_Secret=qmsdlkqmlksdvnnpae ME_Api=https://api.dnsmadeeasy.com/V2.0/dns/managed @@ -114,7 +114,7 @@ _get_root() { fi if _contains "$response" "\"name\":\"$h\""; then - _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*" | head -n 1 | cut -d : -f 2 | tr -d '}') + _domain_id=$(printf "%s\n" "$response" | sed 's/^{//; s/}$//; s/{.*}//' | sed -r 's/^.*"id":([0-9]+).*$/\1/') if [ "$_domain_id" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) _domain="$h" diff --git a/dnsapi/dns_miab.sh b/dnsapi/dns_miab.sh new file mode 100644 index 00000000..dad69bde --- /dev/null +++ b/dnsapi/dns_miab.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env sh + +# Name: dns_miab.sh +# +# Authors: +# Darven Dissek 2018 +# William Gertz 2019 +# +# Thanks to Neil Pang and other developers here for code reused from acme.sh from DNS-01 +# used to communicate with the MailinaBox Custom DNS API +# Report Bugs here: +# https://github.com/billgertz/MIAB_dns_api (for dns_miab.sh) +# https://github.com/acmesh-official/acme.sh (for acme.sh) +# +######## Public functions ##################### + +#Usage: dns_miab_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_miab_add() { + fulldomain=$1 + txtvalue=$2 + _info "Using miab challange add" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + #retrieve MIAB environemt vars + if ! _retrieve_miab_env; then + return 1 + fi + + #check domain and seperate into doamin and host + if ! _get_root "$fulldomain"; then + _err "Cannot find any part of ${fulldomain} is hosted on ${MIAB_Server}" + return 1 + fi + + _debug2 _sub_domain "$_sub_domain" + _debug2 _domain "$_domain" + + #add the challenge record + _api_path="custom/${fulldomain}/txt" + _miab_rest "$txtvalue" "$_api_path" "POST" + + #check if result was good + if _contains "$response" "updated DNS"; then + _info "Successfully created the txt record" + return 0 + else + _err "Error encountered during record add" + _err "$response" + return 1 + fi +} + +#Usage: dns_miab_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_miab_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Using miab challage delete" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + #retrieve MIAB environemt vars + if ! _retrieve_miab_env; then + return 1 + fi + + #check domain and seperate into doamin and host + if ! _get_root "$fulldomain"; then + _err "Cannot find any part of ${fulldomain} is hosted on ${MIAB_Server}" + return 1 + fi + + _debug2 _sub_domain "$_sub_domain" + _debug2 _domain "$_domain" + + #Remove the challenge record + _api_path="custom/${fulldomain}/txt" + _miab_rest "$txtvalue" "$_api_path" "DELETE" + + #check if result was good + if _contains "$response" "updated DNS"; then + _info "Successfully removed the txt record" + return 0 + else + _err "Error encountered during record remove" + _err "$response" + return 1 + fi +} + +#################### Private functions below ################################## +# +#Usage: _get_root _acme-challenge.www.domain.com +#Returns: +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + _passed_domain=$1 + _debug _passed_domain "$_passed_domain" + _i=2 + _p=1 + + #get the zones hosed on MIAB server, must be a json stream + _miab_rest "" "zones" "GET" + + if ! _is_json "$response"; then + _err "ERROR fetching domain list" + _err "$response" + return 1 + fi + + #cycle through the passed domain seperating out a test domain discarding + # the subdomain by marching thorugh the dots + while true; do + _test_domain=$(printf "%s" "$_passed_domain" | cut -d . -f ${_i}-100) + _debug _test_domain "$_test_domain" + + if [ -z "$_test_domain" ]; then + return 1 + fi + + #report found if the test domain is in the json response and + # report the subdomain + if _contains "$response" "\"$_test_domain\""; then + _sub_domain=$(printf "%s" "$_passed_domain" | cut -d . -f 1-${_p}) + _domain=${_test_domain} + return 0 + fi + + #cycle to the next dot in the passed domain + _p=${_i} + _i=$(_math "$_i" + 1) + done + + return 1 +} + +#Usage: _retrieve_miab_env +#Returns (from store or environment variables): +# MIAB_Username +# MIAB_Password +# MIAB_Server +#retrieve MIAB environment variables, report errors and quit if problems +_retrieve_miab_env() { + MIAB_Username="${MIAB_Username:-$(_readaccountconf_mutable MIAB_Username)}" + MIAB_Password="${MIAB_Password:-$(_readaccountconf_mutable MIAB_Password)}" + MIAB_Server="${MIAB_Server:-$(_readaccountconf_mutable MIAB_Server)}" + + #debug log the environmental variables + _debug MIAB_Username "$MIAB_Username" + _debug MIAB_Password "$MIAB_Password" + _debug MIAB_Server "$MIAB_Server" + + #check if MIAB environemt vars set and quit if not + if [ -z "$MIAB_Username" ] || [ -z "$MIAB_Password" ] || [ -z "$MIAB_Server" ]; then + _err "You didn't specify one or more of MIAB_Username, MIAB_Password or MIAB_Server." + _err "Please check these environment variables and try again." + return 1 + fi + + #save the credentials to the account conf file. + _saveaccountconf_mutable MIAB_Username "$MIAB_Username" + _saveaccountconf_mutable MIAB_Password "$MIAB_Password" + _saveaccountconf_mutable MIAB_Server "$MIAB_Server" + return 0 +} + +#Useage: _miab_rest "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" "custom/_acme-challenge.www.domain.com/txt "POST" +#Returns: "updated DNS: domain.com" +#rest interface MIAB dns +_miab_rest() { + _data="$1" + _api_path="$2" + _httpmethod="$3" + + #encode username and password for basic authentication + _credentials="$(printf "%s" "$MIAB_Username:$MIAB_Password" | _base64)" + export _H1="Authorization: Basic $_credentials" + _url="https://${MIAB_Server}/admin/dns/${_api_path}" + + _debug2 _data "$_data" + _debug _api_path "$_api_path" + _debug2 _url "$_url" + _debug2 _credentails "$_credentials" + _debug _httpmethod "$_httpmethod" + + if [ "$_httpmethod" = "GET" ]; then + response="$(_get "$_url")" + else + response="$(_post "$_data" "$_url" "" "$_httpmethod")" + fi + + _retcode="$?" + + if [ "$_retcode" != "0" ]; then + _err "MIAB REST authentication failed on $_httpmethod" + return 1 + fi + + _debug response "$response" + return 0 +} + +#Usage: _is_json "\[\n "mydomain.com"\n]" +#Reurns "\[\n "mydomain.com"\n]" +#returns the string if it begins and ends with square braces +_is_json() { + _str="$(echo "$1" | _normalizeJson)" + echo "$_str" | grep '^\[.*\]$' >/dev/null 2>&1 +} diff --git a/dnsapi/dns_misaka.sh b/dnsapi/dns_misaka.sh new file mode 100755 index 00000000..36ba5cfd --- /dev/null +++ b/dnsapi/dns_misaka.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env sh + +# bug reports to support+acmesh@misaka.io +# based on dns_nsone.sh by dev@1e.ca + +# +#Misaka_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" +# + +Misaka_Api="https://dnsapi.misaka.io/dns" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_misaka_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$Misaka_Key" ]; then + Misaka_Key="" + _err "You didn't specify misaka.io dns api key yet." + _err "Please create you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf Misaka_Key "$Misaka_Key" + + _debug "checking root zone [$fulldomain]" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _misaka_rest GET "zones/${_domain}/recordsets?search=${_sub_domain}" + + if ! _contains "$response" "\"results\":"; then + _err "Error" + return 1 + fi + + count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$_sub_domain\",[^{]*\"type\":\"TXT\"" | wc -l | tr -d " ") + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Adding record" + + if _misaka_rest POST "zones/${_domain}/recordsets/${_sub_domain}/TXT" "{\"records\":[{\"value\":\"\\\"$txtvalue\\\"\"}],\"filters\":[],\"ttl\":1}"; then + _debug response "$response" + if _contains "$response" "$_sub_domain"; then + _info "Added" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + else + _info "Updating record" + + _misaka_rest PUT "zones/${_domain}/recordsets/${_sub_domain}/TXT?append=true" "{\"records\": [{\"value\": \"\\\"$txtvalue\\\"\"}],\"ttl\":1}" + if [ "$?" = "0" ] && _contains "$response" "$_sub_domain"; then + _info "Updated!" + #todo: check if the record takes effect + return 0 + fi + _err "Update error" + return 1 + fi + +} + +#fulldomain +dns_misaka_rm() { + fulldomain=$1 + txtvalue=$2 + _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" + _misaka_rest GET "zones/${_domain}/recordsets?search=${_sub_domain}" + + count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$_sub_domain\",[^{]*\"type\":\"TXT\"" | wc -l | tr -d " ") + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + if ! _misaka_rest DELETE "zones/${_domain}/recordsets/${_sub_domain}/TXT"; then + _err "Delete record error." + return 1 + fi + _contains "$response" "" + 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 ! _misaka_rest GET "zones?limit=1000"; 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 + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_misaka_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Content-Type: application/json" + export _H2="User-Agent: acme.sh/$VER misaka-dns-acmesh/20191213" + export _H3="Authorization: Token $Misaka_Key" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$Misaka_Api/$ep" "" "$m")" + else + response="$(_get "$Misaka_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_myapi.sh b/dnsapi/dns_myapi.sh index 6bf62508..7f3c5a86 100755 --- a/dnsapi/dns_myapi.sh +++ b/dnsapi/dns_myapi.sh @@ -7,10 +7,12 @@ #returns 0 means success, otherwise error. # #Author: Neilpang -#Report Bugs here: https://github.com/Neilpang/acme.sh +#Report Bugs here: https://github.com/acmesh-official/acme.sh # ######## Public functions ##################### +# Please Read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide + #Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_myapi_add() { fulldomain=$1 diff --git a/dnsapi/dns_mydevil.sh b/dnsapi/dns_mydevil.sh new file mode 100755 index 00000000..953290af --- /dev/null +++ b/dnsapi/dns_mydevil.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env sh + +# MyDevil.net API (2019-02-03) +# +# MyDevil.net already supports automatic Let's Encrypt certificates, +# except for wildcard domains. +# +# This script depends on `devil` command that MyDevil.net provides, +# which means that it works only on server side. +# +# Author: Marcin Konicki +# +######## Public functions ##################### + +#Usage: dns_mydevil_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_mydevil_add() { + fulldomain=$1 + txtvalue=$2 + domain="" + + if ! _exists "devil"; then + _err "Could not find 'devil' command." + return 1 + fi + + _info "Using mydevil" + + domain=$(mydevil_get_domain "$fulldomain") + if [ -z "$domain" ]; then + _err "Invalid domain name: could not find root domain of $fulldomain." + return 1 + fi + + # No need to check if record name exists, `devil` always adds new record. + # In worst case scenario, we end up with multiple identical records. + + _info "Adding $fulldomain record for domain $domain" + if devil dns add "$domain" "$fulldomain" TXT "$txtvalue"; then + _info "Successfully added TXT record, ready for validation." + return 0 + else + _err "Unable to add DNS record." + return 1 + fi +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_mydevil_rm() { + fulldomain=$1 + txtvalue=$2 + domain="" + + if ! _exists "devil"; then + _err "Could not find 'devil' command." + return 1 + fi + + _info "Using mydevil" + + domain=$(mydevil_get_domain "$fulldomain") + if [ -z "$domain" ]; then + _err "Invalid domain name: could not find root domain of $fulldomain." + return 1 + fi + + # catch one or more numbers + num='[0-9][0-9]*' + # catch one or more whitespace + w=$(printf '[\t ][\t ]*') + # catch anything, except newline + any='.*' + # filter to make sure we do not delete other records + validRecords="^${num}${w}${fulldomain}${w}TXT${w}${any}${txtvalue}$" + for id in $(devil dns list "$domain" | tail -n+2 | grep "${validRecords}" | cut -w -s -f 1); do + _info "Removing record $id from domain $domain" + echo "y" | devil dns del "$domain" "$id" || _err "Could not remove DNS record." + done +} + +#################### Private functions below ################################## + +# Usage: domain=$(mydevil_get_domain "_acme-challenge.www.domain.com" || _err "Invalid domain name") +# echo $domain +mydevil_get_domain() { + fulldomain=$1 + domain="" + + for domain in $(devil dns list | cut -w -s -f 1 | tail -n+2); do + _debug "Checking domain: $domain" + if _endswith "$fulldomain" "$domain"; then + _debug "Fulldomain '$fulldomain' matches '$domain'" + printf -- "%s" "$domain" + return 0 + fi + done + + return 1 +} diff --git a/dnsapi/dns_mydnsjp.sh b/dnsapi/dns_mydnsjp.sh new file mode 100755 index 00000000..13866f70 --- /dev/null +++ b/dnsapi/dns_mydnsjp.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env sh + +#Here is a api script for MyDNS.JP. +#This file name is "dns_mydnsjp.sh" +#So, here must be a method dns_mydnsjp_add() +#Which will be called by acme.sh to add the txt record to your api system. +#returns 0 means success, otherwise error. +# +#Author: epgdatacapbon +#Report Bugs here: https://github.com/epgdatacapbon/acme.sh +# +######## Public functions ##################### + +# Export MyDNS.JP MasterID and Password in following variables... +# MYDNSJP_MasterID=MasterID +# MYDNSJP_Password=Password + +MYDNSJP_API="https://www.mydns.jp" + +#Usage: dns_mydnsjp_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_mydnsjp_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Using mydnsjp" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + # Load the credentials from the account conf file + MYDNSJP_MasterID="${MYDNSJP_MasterID:-$(_readaccountconf_mutable MYDNSJP_MasterID)}" + MYDNSJP_Password="${MYDNSJP_Password:-$(_readaccountconf_mutable MYDNSJP_Password)}" + if [ -z "$MYDNSJP_MasterID" ] || [ -z "$MYDNSJP_Password" ]; then + MYDNSJP_MasterID="" + MYDNSJP_Password="" + _err "You don't specify mydnsjp api MasterID and Password yet." + _err "Please export as MYDNSJP_MasterID / MYDNSJP_Password and try again." + return 1 + fi + + # Save the credentials to the account conf file + _saveaccountconf_mutable MYDNSJP_MasterID "$MYDNSJP_MasterID" + _saveaccountconf_mutable MYDNSJP_Password "$MYDNSJP_Password" + + _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" + + if _mydnsjp_api "REGIST" "$_domain" "$txtvalue"; 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 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_mydnsjp_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Removing TXT record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + # Load the credentials from the account conf file + MYDNSJP_MasterID="${MYDNSJP_MasterID:-$(_readaccountconf_mutable MYDNSJP_MasterID)}" + MYDNSJP_Password="${MYDNSJP_Password:-$(_readaccountconf_mutable MYDNSJP_Password)}" + if [ -z "$MYDNSJP_MasterID" ] || [ -z "$MYDNSJP_Password" ]; then + MYDNSJP_MasterID="" + MYDNSJP_Password="" + _err "You don't specify mydnsjp api MasterID and Password yet." + _err "Please export as MYDNSJP_MasterID / MYDNSJP_Password and try again." + 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" + + if _mydnsjp_api "DELETE" "$_domain" "$txtvalue"; then + if printf -- "%s" "$response" | grep "OK." >/dev/null; then + _info "Deleted, OK" + return 0 + else + _err "Delete txt record error." + return 1 + fi + fi + _err "Delete txt record error." + + return 1 +} + +#################### Private functions below ################################## +# _acme-challenge.www.domain.com +# returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + fulldomain=$1 + i=2 + p=1 + + # Get the root domain + _mydnsjp_retrieve_domain + if [ "$?" != "0" ]; then + # not valid + return 1 + fi + + while true; do + _domain=$(printf "%s" "$fulldomain" | cut -d . -f $i-100) + + if [ -z "$_domain" ]; then + # not valid + return 1 + fi + + if [ "$_domain" = "$_root_domain" ]; then + _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-$p) + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done + + return 1 +} + +# Retrieve the root domain +# returns 0 success +_mydnsjp_retrieve_domain() { + _debug "Login to MyDNS.JP" + + response="$(_post "MENU=100&masterid=$MYDNSJP_MasterID&masterpwd=$MYDNSJP_Password" "$MYDNSJP_API/members/")" + cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)" + + # If cookies is not empty then logon successful + if [ -z "$cookie" ]; then + _err "Fail to get a cookie." + return 1 + fi + + _root_domain=$(echo "$response" | grep "DNSINFO\[domainname\]" | sed 's/^.*value="\([^"]*\)".*/\1/') + + _debug _root_domain "$_root_domain" + + if [ -z "$_root_domain" ]; then + _err "Fail to get the root domain." + return 1 + fi + + return 0 +} + +_mydnsjp_api() { + cmd=$1 + domain=$2 + txtvalue=$3 + + # Base64 encode the credentials + credentials=$(printf "%s:%s" "$MYDNSJP_MasterID" "$MYDNSJP_Password" | _base64) + + # Construct the HTTP Authorization header + export _H1="Content-Type: application/x-www-form-urlencoded" + export _H2="Authorization: Basic ${credentials}" + + response="$(_post "CERTBOT_DOMAIN=$domain&CERTBOT_VALIDATION=$txtvalue&EDIT_CMD=$cmd" "$MYDNSJP_API/directedit.html")" + + if [ "$?" != "0" ]; then + _err "error $domain" + return 1 + fi + + _debug2 response "$response" + + return 0 +} diff --git a/dnsapi/dns_mythic_beasts.sh b/dnsapi/dns_mythic_beasts.sh new file mode 100755 index 00000000..294ae84c --- /dev/null +++ b/dnsapi/dns_mythic_beasts.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env sh +# Mythic Beasts is a long-standing UK service provider using standards-based OAuth2 authentication +# To test: ./acme.sh --dns dns_mythic_beasts --test --debug 1 --output-insecure --issue --domain domain.com +# Cannot retest once cert is issued +# OAuth2 tokens only valid for 300 seconds so we do not store +# NOTE: This will remove all TXT records matching the fulldomain, not just the added ones (_acme-challenge.www.domain.com) + +# Test OAuth2 credentials +#MB_AK="aaaaaaaaaaaaaaaa" +#MB_AS="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + +# URLs +MB_API='https://api.mythic-beasts.com/dns/v2/zones' +MB_AUTH='https://auth.mythic-beasts.com/login' + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_mythic_beasts_add() { + fulldomain=$1 + txtvalue=$2 + + _info "MYTHIC BEASTS Adding record $fulldomain = $txtvalue" + if ! _initAuth; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + return 1 + fi + + # method path body_data + if _mb_rest POST "$_domain/records/$_sub_domain/TXT" "$txtvalue"; then + + if _contains "$response" "1 records added"; then + _info "Added, verifying..." + # Max 120 seconds to publish + for i in $(seq 1 6); do + # Retry on error + if ! _mb_rest GET "$_domain/records/$_sub_domain/TXT?verify"; then + _sleep 20 + else + _info "Record published!" + return 0 + fi + done + + else + _err "\n$response" + fi + + fi + _err "Add txt record error." + return 1 +} + +#Usage: rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_mythic_beasts_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "MYTHIC BEASTS Removing record $fulldomain = $txtvalue" + if ! _initAuth; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + return 1 + fi + + # method path body_data + if _mb_rest DELETE "$_domain/records/$_sub_domain/TXT" "$txtvalue"; then + _info "Record removed" + return 0 + fi + _err "Remove txt record error." + return 1 +} + +#################### Private functions below ################################## + +#Possible formats: +# _acme-challenge.www.example.com +# _acme-challenge.example.com +# _acme-challenge.example.co.uk +# _acme-challenge.www.example.co.uk +# _acme-challenge.sub1.sub2.www.example.co.uk +# sub1.sub2.example.co.uk +# example.com +# example.co.uk +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + p=1 + + _debug "Detect the root zone" + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + _err "Domain exhausted" + return 1 + fi + + # Use the status errors to find the domain, continue on 403 Access denied + # method path body_data + _mb_rest GET "$h/records" + ret="$?" + if [ "$ret" -eq 0 ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + return 0 + elif [ "$ret" -eq 1 ]; then + return 1 + fi + + p=$i + i=$(_math "$i" + 1) + + if [ "$i" -gt 50 ]; then + break + fi + done + _err "Domain too long" + return 1 +} + +_initAuth() { + MB_AK="${MB_AK:-$(_readaccountconf_mutable MB_AK)}" + MB_AS="${MB_AS:-$(_readaccountconf_mutable MB_AS)}" + + if [ -z "$MB_AK" ] || [ -z "$MB_AS" ]; then + MB_AK="" + MB_AS="" + _err "Please specify an OAuth2 Key & Secret" + return 1 + fi + + _saveaccountconf_mutable MB_AK "$MB_AK" + _saveaccountconf_mutable MB_AS "$MB_AS" + + if ! _oauth2; then + return 1 + fi + + _info "Checking authentication" + _secure_debug access_token "$MB_TK" + _sleep 1 + + # GET a list of zones + # method path body_data + if ! _mb_rest GET ""; then + _err "The token is invalid" + return 1 + fi + _info "Token OK" + return 0 +} + +# Github appears to use an outbound proxy for requests which means subsequent requests may not have the same +# source IP. The standard Mythic Beasts OAuth2 tokens are tied to an IP, meaning github test requests fail +# authentication. This is a work around using an undocumented MB API to obtain a token not tied to an +# IP just for the github tests. +_oauth2() { + if [ "$GITHUB_ACTIONS" = "true" ]; then + _oauth2_github + else + _oauth2_std + fi + return $? +} + +_oauth2_std() { + # HTTP Basic Authentication + _H1="Authorization: Basic $(echo "$MB_AK:$MB_AS" | _base64)" + _H2="Accepts: application/json" + export _H1 _H2 + body="grant_type=client_credentials" + + _info "Getting OAuth2 token..." + # body url [needbase64] [POST|PUT|DELETE] [ContentType] + response="$(_post "$body" "$MB_AUTH" "" "POST" "application/x-www-form-urlencoded")" + if _contains "$response" "\"token_type\":\"bearer\""; then + MB_TK="$(echo "$response" | _egrep_o "access_token\":\"[^\"]*\"" | cut -d : -f 2 | tr -d '"')" + if [ -z "$MB_TK" ]; then + _err "Unable to get access_token" + _err "\n$response" + return 1 + fi + else + _err "OAuth2 token_type not Bearer" + _err "\n$response" + return 1 + fi + _debug2 response "$response" + return 0 +} + +_oauth2_github() { + _H1="Accepts: application/json" + export _H1 + body="{\"login\":{\"handle\":\"$MB_AK\",\"pass\":\"$MB_AS\",\"floating\":1}}" + + _info "Getting Floating token..." + # body url [needbase64] [POST|PUT|DELETE] [ContentType] + response="$(_post "$body" "$MB_AUTH" "" "POST" "application/json")" + MB_TK="$(echo "$response" | _egrep_o "\"token\":\"[^\"]*\"" | cut -d : -f 2 | tr -d '"')" + if [ -z "$MB_TK" ]; then + _err "Unable to get token" + _err "\n$response" + return 1 + fi + _debug2 response "$response" + return 0 +} + +# method path body_data +_mb_rest() { + # URL encoded body for single API operations + m="$1" + ep="$2" + data="$3" + + if [ -z "$ep" ]; then + _mb_url="$MB_API" + else + _mb_url="$MB_API/$ep" + fi + + _H1="Authorization: Bearer $MB_TK" + _H2="Accepts: application/json" + export _H1 _H2 + if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ] || [ "$m" = "DELETE" ]; then + # body url [needbase64] [POST|PUT|DELETE] [ContentType] + response="$(_post "data=$data" "$_mb_url" "" "$m" "application/x-www-form-urlencoded")" + else + response="$(_get "$_mb_url")" + fi + + if [ "$?" != "0" ]; then + _err "Request error" + return 1 + fi + + header="$(cat "$HTTP_HEADER")" + status="$(echo "$header" | _egrep_o "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n")" + code="$(echo "$status" | _egrep_o "^[0-9]*")" + if [ "$code" -ge 400 ] || _contains "$response" "\"error\"" || _contains "$response" "invalid_client"; then + _err "error $status" + _err "\n$response" + _debug "\n$header" + return 2 + fi + + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_namecheap.sh b/dnsapi/dns_namecheap.sh index 7089c2d0..a5f667a9 100755 --- a/dnsapi/dns_namecheap.sh +++ b/dnsapi/dns_namecheap.sh @@ -3,16 +3,15 @@ # Namecheap API # https://www.namecheap.com/support/api/intro.aspx # -# Requires Namecheap API key set in NAMECHEAP_API_KEY, NAMECHEAP_SOURCEIP and NAMECHEAP_USERNAME set as environment variable +# Requires Namecheap API key set in +#NAMECHEAP_API_KEY, +#NAMECHEAP_USERNAME, +#NAMECHEAP_SOURCEIP # Due to Namecheap's API limitation all the records of your domain will be read and re applied, make sure to have a backup of your records you could apply if any issue would arise. ######## Public functions ##################### -if [ "$STAGE" -eq 1 ]; then - NAMECHEAP_API="https://api.sandbox.namecheap.com/xml.response" -else - NAMECHEAP_API="https://api.namecheap.com/xml.response" -fi +NAMECHEAP_API="https://api.namecheap.com/xml.response" #Usage: dns_namecheap_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_namecheap_add() { @@ -77,6 +76,22 @@ dns_namecheap_rm() { # _sub_domain=_acme-challenge.www # _domain=domain.com _get_root() { + fulldomain=$1 + + if ! _get_root_by_getList "$fulldomain"; then + _debug "Failed domain lookup via domains.getList api call. Trying domain lookup via domains.dns.getHosts api." + # The above "getList" api will only return hosts *owned* by the calling user. However, if the calling + # user is not the owner, but still has administrative rights, we must query the getHosts api directly. + # See this comment and the official namecheap response: https://disq.us/p/1q6v9x9 + if ! _get_root_by_getHosts "$fulldomain"; then + return 1 + fi + fi + + return 0 +} + +_get_root_by_getList() { domain=$1 if ! _namecheap_post "namecheap.domains.getList"; then @@ -95,6 +110,10 @@ _get_root() { #not valid return 1 fi + if ! _contains "$h" "\\."; then + #not valid + return 1 + fi if ! _contains "$response" "$h"; then _debug "$h not found" @@ -109,18 +128,43 @@ _get_root() { return 1 } +_get_root_by_getHosts() { + i=100 + p=99 + + while [ $p -ne 0 ]; do + + h=$(printf "%s" "$1" | cut -d . -f $i-100) + if [ -n "$h" ]; then + if _contains "$h" "\\."; then + _debug h "$h" + if _namecheap_set_tld_sld "$h"; then + _sub_domain=$(printf "%s" "$1" | cut -d . -f 1-$p) + _domain="$h" + return 0 + else + _debug "$h not found" + fi + fi + fi + i="$p" + p=$(_math "$p" - 1) + done + return 1 +} + _namecheap_set_publicip() { if [ -z "$NAMECHEAP_SOURCEIP" ]; then _err "No Source IP specified for Namecheap API." - _err "Use your public ip address or an url to retrieve it (e.g. https://ipconfig.co/ip) and export it as NAMECHEAP_SOURCEIP" + _err "Use your public ip address or an url to retrieve it (e.g. https://ifconfig.co/ip) and export it as NAMECHEAP_SOURCEIP" return 1 else _saveaccountconf NAMECHEAP_SOURCEIP "$NAMECHEAP_SOURCEIP" _debug sourceip "$NAMECHEAP_SOURCEIP" ip=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}') - addr=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '(http|https)://.*') + addr=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '(http|https):\/\/.*') _debug2 ip "$ip" _debug2 addr "$addr" @@ -131,7 +175,7 @@ _namecheap_set_publicip() { _publicip=$(_get "$addr") else _err "No Source IP specified for Namecheap API." - _err "Use your public ip address or an url to retrieve it (e.g. https://ipconfig.co/ip) and export it as NAMECHEAP_SOURCEIP" + _err "Use your public ip address or an url to retrieve it (e.g. https://ifconfig.co/ip) and export it as NAMECHEAP_SOURCEIP" return 1 fi fi @@ -144,7 +188,7 @@ _namecheap_set_publicip() { _namecheap_post() { command=$1 data="ApiUser=${NAMECHEAP_USERNAME}&ApiKey=${NAMECHEAP_API_KEY}&ClientIp=${_publicip}&UserName=${NAMECHEAP_USERNAME}&Command=${command}" - + _debug2 "_namecheap_post data" "$data" response="$(_post "$data" "$NAMECHEAP_API" "" "POST")" _debug2 response "$response" @@ -161,12 +205,12 @@ _namecheap_parse_host() { _host=$1 _debug _host "$_host" - _hostid=$(echo "$_host" | _egrep_o '\sHostId="[^"]*' | cut -d '"' -f 2) - _hostname=$(echo "$_host" | _egrep_o '\sName="[^"]*' | cut -d '"' -f 2) - _hosttype=$(echo "$_host" | _egrep_o '\sType="[^"]*' | cut -d '"' -f 2) - _hostaddress=$(echo "$_host" | _egrep_o '\sAddress="[^"]*' | cut -d '"' -f 2) - _hostmxpref=$(echo "$_host" | _egrep_o '\sMXPref="[^"]*' | cut -d '"' -f 2) - _hostttl=$(echo "$_host" | _egrep_o '\sTTL="[^"]*' | cut -d '"' -f 2) + _hostid=$(echo "$_host" | _egrep_o ' HostId="[^"]*' | cut -d '"' -f 2) + _hostname=$(echo "$_host" | _egrep_o ' Name="[^"]*' | cut -d '"' -f 2) + _hosttype=$(echo "$_host" | _egrep_o ' Type="[^"]*' | cut -d '"' -f 2) + _hostaddress=$(echo "$_host" | _egrep_o ' Address="[^"]*' | cut -d '"' -f 2 | _xml_decode) + _hostmxpref=$(echo "$_host" | _egrep_o ' MXPref="[^"]*' | cut -d '"' -f 2) + _hostttl=$(echo "$_host" | _egrep_o ' TTL="[^"]*' | cut -d '"' -f 2) _debug hostid "$_hostid" _debug hostname "$_hostname" @@ -199,9 +243,12 @@ _namecheap_check_config() { _set_namecheap_TXT() { subdomain=$2 txt=$3 - tld=$(echo "$1" | cut -d '.' -f 2) - sld=$(echo "$1" | cut -d '.' -f 1) - request="namecheap.domains.dns.getHosts&SLD=$sld&TLD=$tld" + + if ! _namecheap_set_tld_sld "$1"; then + return 1 + fi + + request="namecheap.domains.dns.getHosts&SLD=${_sld}&TLD=${_tld}" if ! _namecheap_post "$request"; then _err "$error" @@ -212,7 +259,7 @@ _set_namecheap_TXT() { _debug hosts "$hosts" if [ -z "$hosts" ]; then - _error "Hosts not found" + _err "Hosts not found" return 1 fi @@ -221,6 +268,12 @@ _set_namecheap_TXT() { while read -r host; do if _contains "$host" "300") if [ "$retcode" ]; then - _record_id=$(printf "%s\n" "$response" | _egrep_o "([^<]*)TXT$fulldomain" | _egrep_o "([^<]*)" | sed -r "s/([^<]*)<\/record_id>/\1/" | tail -n 1) - _debug record_id "$_record_id" - _info "Successfully retrieved the record id for ACME challenge." + _record_id=$(echo "$response" | _egrep_o "([^<]*)TXT$fulldomain" | _egrep_o "([^<]*)" | sed -r "s/([^<]*)<\/record_id>/\1/" | tail -n 1) + _debug _record_id "$_record_id" + if [ "$_record_id" ]; then + _info "Successfully retrieved the record id for ACME challenge." + else + _info "Empty record id, it seems no such record." + return 0 + fi else _err "Unable to retrieve the record id." return 1 @@ -105,7 +110,7 @@ _get_root() { return 1 fi - if _contains "$response" "$host"; then + if _contains "$response" ">$host"; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) _domain="$host" return 0 diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh new file mode 100644 index 00000000..8ccc8c29 --- /dev/null +++ b/dnsapi/dns_nanelo.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env sh + +# Official DNS API for Nanelo.com + +# Provide the required API Key like this: +# NANELO_TOKEN="FmD408PdqT1E269gUK57" + +NANELO_API="https://api.nanelo.com/v1/" + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_nanelo_add() { + fulldomain=$1 + txtvalue=$2 + + NANELO_TOKEN="${NANELO_TOKEN:-$(_readaccountconf_mutable NANELO_TOKEN)}" + if [ -z "$NANELO_TOKEN" ]; then + NANELO_TOKEN="" + _err "You didn't configure a Nanelo API Key yet." + _err "Please set NANELO_TOKEN and try again." + _err "Login to Nanelo.com and go to Settings > API Keys to get a Key" + return 1 + fi + _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" + + _info "Adding TXT record to ${fulldomain}" + response="$(_get "$NANELO_API$NANELO_TOKEN/dns/addrecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" + if _contains "${response}" 'success'; then + return 0 + fi + _err "Could not create resource record, please check the logs" + _err "${response}" + return 1 +} + +dns_nanelo_rm() { + fulldomain=$1 + txtvalue=$2 + + NANELO_TOKEN="${NANELO_TOKEN:-$(_readaccountconf_mutable NANELO_TOKEN)}" + if [ -z "$NANELO_TOKEN" ]; then + NANELO_TOKEN="" + _err "You didn't configure a Nanelo API Key yet." + _err "Please set NANELO_TOKEN and try again." + _err "Login to Nanelo.com and go to Settings > API Keys to get a Key" + return 1 + fi + _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" + + _info "Deleting resource record $fulldomain" + response="$(_get "$NANELO_API$NANELO_TOKEN/dns/deleterecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")" + if _contains "${response}" 'success'; then + return 0 + fi + _err "Could not delete resource record, please check the logs" + _err "${response}" + return 1 +} diff --git a/dnsapi/dns_nederhost.sh b/dnsapi/dns_nederhost.sh new file mode 100755 index 00000000..abaae42b --- /dev/null +++ b/dnsapi/dns_nederhost.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env sh + +#NederHost_Key="sdfgikogfdfghjklkjhgfcdcfghj" + +NederHost_Api="https://api.nederhost.nl/dns/v1" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_nederhost_add() { + fulldomain=$1 + txtvalue=$2 + + NederHost_Key="${NederHost_Key:-$(_readaccountconf_mutable NederHost_Key)}" + if [ -z "$NederHost_Key" ]; then + NederHost_Key="" + _err "You didn't specify a NederHost api key." + _err "You can get yours from https://www.nederhost.nl/mijn_nederhost" + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable NederHost_Key "$NederHost_Key" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + if _nederhost_rest PATCH "zones/$_domain/records/$fulldomain/TXT" "[{\"content\":\"$txtvalue\",\"ttl\":60}]"; then + if _contains "$response" "$fulldomain"; then + _info "Added, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + return 1 + +} + +#fulldomain txtvalue +dns_nederhost_rm() { + fulldomain=$1 + txtvalue=$2 + + NederHost_Key="${NederHost_Key:-$(_readaccountconf_mutable NederHost_Key)}" + if [ -z "$NederHost_Key" ]; then + NederHost_Key="" + _err "You didn't specify a NederHost api key." + _err "You can get yours from https://www.nederhost.nl/mijn_nederhost" + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Removing txt record" + _nederhost_rest DELETE "zones/${_domain}/records/$fulldomain/TXT?content=$txtvalue" + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + _domain=$(printf "%s" "$domain" | cut -d . -f $i-100) + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _debug _domain "$_domain" + if [ -z "$_domain" ]; then + #not valid + return 1 + fi + + if _nederhost_rest GET "zones/${_domain}"; then + if [ "${_code}" = "204" ]; then + return 0 + fi + else + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_nederhost_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Authorization: Bearer $NederHost_Key" + export _H2="Content-Type: application/json" + + _debug data "$data" + response="$(_post "$data" "$NederHost_Api/$ep" "" "$m")" + + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug "http response code $_code" + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_neodigit.sh b/dnsapi/dns_neodigit.sh new file mode 100644 index 00000000..64ea8786 --- /dev/null +++ b/dnsapi/dns_neodigit.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env sh + +# +# NEODIGIT_API_TOKEN="jasdfhklsjadhflnhsausdfas" + +# This is Neodigit.net api wrapper for acme.sh +# +# Author: Adrian Almenar +# Report Bugs here: https://github.com/tecnocratica/acme.sh +# +NEODIGIT_API_URL="https://api.neodigit.net/v1" +# +######## Public functions ##################### + +# Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_neodigit_add() { + fulldomain=$1 + txtvalue=$2 + + NEODIGIT_API_TOKEN="${NEODIGIT_API_TOKEN:-$(_readaccountconf_mutable NEODIGIT_API_TOKEN)}" + if [ -z "$NEODIGIT_API_TOKEN" ]; then + NEODIGIT_API_TOKEN="" + _err "You haven't specified a Token api key." + _err "Please create the key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable NEODIGIT_API_TOKEN "$NEODIGIT_API_TOKEN" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + _debug domain "$_domain" + _debug sub_domain "$_sub_domain" + + _debug "Getting txt records" + _neo_rest GET "dns/zones/${_domain_id}/records?type=TXT&name=$fulldomain" + + _debug _code "$_code" + + if [ "$_code" != "200" ]; then + _err "error retrieving data!" + return 1 + fi + + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + _debug domain "$_domain" + _debug sub_domain "$_sub_domain" + + _info "Adding record" + if _neo_rest POST "dns/zones/$_domain_id/records" "{\"record\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":60}}"; then + if printf -- "%s" "$response" | grep "$_sub_domain" >/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 +} + +#fulldomain txtvalue +dns_neodigit_rm() { + fulldomain=$1 + txtvalue=$2 + + NEODIGIT_API_TOKEN="${NEODIGIT_API_TOKEN:-$(_readaccountconf_mutable NEODIGIT_API_TOKEN)}" + if [ -z "$NEODIGIT_API_TOKEN" ]; then + NEODIGIT_API_TOKEN="" + _err "You haven't specified a Token api key." + _err "Please create the key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable NEODIGIT_API_TOKEN "$NEODIGIT_API_TOKEN" + + _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" + _neo_rest GET "dns/zones/${_domain_id}/records?type=TXT&name=$fulldomain&content=$txtvalue" + + if [ "$_code" != "200" ]; then + _err "error retrieving data!" + return 1 + fi + + record_id=$(echo "$response" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d: -f2 | cut -d, -f1) + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! _neo_rest DELETE "dns/zones/$_domain_id/records/$record_id"; then + _err "Delete record error." + return 1 + fi + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=dasfdsafsadg5ythd +_get_root() { + domain=$1 + i=2 + 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 ! _neo_rest GET "dns/zones?name=$h"; then + return 1 + fi + + _debug p "$p" + + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + _domain_id=$(echo "$response" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d: -f2 | cut -d, -f1) + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_neo_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="X-TCPanel-Token: $NEODIGIT_API_TOKEN" + export _H2="Content-Type: application/json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$NEODIGIT_API_URL/$ep" "" "$m")" + else + response="$(_get "$NEODIGIT_API_URL/$ep")" + fi + + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_netcup.sh b/dnsapi/dns_netcup.sh index 2273eb7c..776fa02d 100644 --- a/dnsapi/dns_netcup.sh +++ b/dnsapi/dns_netcup.sh @@ -8,6 +8,7 @@ end="https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON" client="" dns_netcup_add() { + _debug NC_Apikey "$NC_Apikey" login if [ "$NC_Apikey" = "" ] || [ "$NC_Apipw" = "" ] || [ "$NC_CID" = "" ]; then _err "No Credentials given" @@ -116,18 +117,18 @@ dns_netcup_rm() { login() { tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST") - sid=$(_getfield "$tmp" "8" | sed s/\"responsedata\":\{\"apisessionid\":\"//g | sed 's/\"\}\}//g') + sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4) _debug "$tmp" - if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then - _err "$msg" + if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then + _err "$tmp" return 1 fi } logout() { tmp=$(_post "{\"action\": \"logout\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST") _debug "$tmp" - if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then - _err "$msg" + if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then + _err "$tmp" return 1 fi } diff --git a/dnsapi/dns_netlify.sh b/dnsapi/dns_netlify.sh new file mode 100644 index 00000000..0e5dc327 --- /dev/null +++ b/dnsapi/dns_netlify.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env sh + +#NETLIFY_ACCESS_TOKEN="xxxx" + +NETLIFY_HOST="api.netlify.com/api/v1/" +NETLIFY_URL="https://$NETLIFY_HOST" + +######## Public functions ##################### + +#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_netlify_add() { + fulldomain=$1 + txtvalue=$2 + + NETLIFY_ACCESS_TOKEN="${NETLIFY_ACCESS_TOKEN:-$(_readaccountconf_mutable NETLIFY_ACCESS_TOKEN)}" + + if [ -z "$NETLIFY_ACCESS_TOKEN" ]; then + NETLIFY_ACCESS_TOKEN="" + _err "Please specify your Netlify Access Token and try again." + return 1 + else + _saveaccountconf_mutable NETLIFY_ACCESS_TOKEN "$NETLIFY_ACCESS_TOKEN" + fi + + _info "Using Netlify" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + dnsRecordURI="dns_zones/$_domain_id/dns_records" + + body="{\"type\":\"TXT\", \"hostname\":\"$_sub_domain\", \"value\":\"$txtvalue\", \"ttl\":\"10\"}" + + _netlify_rest POST "$dnsRecordURI" "$body" "$NETLIFY_ACCESS_TOKEN" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then + _info "validation value added" + return 0 + else + _err "error adding validation value ($_code)" + return 1 + fi + + _err "Not fully implemented!" + return 1 +} + +#Usage: dns_myapi_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +#Remove the txt record after validation. +dns_netlify_rm() { + _info "Using Netlify" + txtdomain="$1" + txt="$2" + _debug txtdomain "$txtdomain" + _debug txt "$txt" + + NETLIFY_ACCESS_TOKEN="${NETLIFY_ACCESS_TOKEN:-$(_readaccountconf_mutable NETLIFY_ACCESS_TOKEN)}" + + if ! _get_root "$txtdomain"; then + _err "invalid domain" + return 1 + fi + + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + dnsRecordURI="dns_zones/$_domain_id/dns_records" + + _netlify_rest GET "$dnsRecordURI" "" "$NETLIFY_ACCESS_TOKEN" + + _record_id=$(echo "$response" | _egrep_o "\"type\":\"TXT\",[^\}]*\"value\":\"$txt\"" | head -n 1 | _egrep_o "\"id\":\"[^\"\}]*\"" | cut -d : -f 2 | tr -d \") + _debug _record_id "$_record_id" + if [ "$_record_id" ]; then + _netlify_rest DELETE "$dnsRecordURI/$_record_id" "" "$NETLIFY_ACCESS_TOKEN" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + if [ "$_code" = "200" ] || [ "$_code" = '204' ]; then + _info "validation value removed" + return 0 + else + _err "error removing validation value ($_code)" + return 1 + fi + return 0 + fi + return 1 +} + +#################### Private functions below ################################## + +_get_root() { + domain=$1 + accesstoken=$2 + i=1 + p=1 + + _netlify_rest GET "dns_zones" "" "$accesstoken" + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug2 "Checking domain: $h" + if [ -z "$h" ]; then + #not valid + _err "Invalid domain" + return 1 + fi + + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + _domain_id=$(echo "$response" | _egrep_o "\"[^\"]*\",\"name\":\"$h\"" | cut -d , -f 1 | tr -d \") + if [ "$_domain_id" ]; then + if [ "$i" = 1 ]; then + #create the record at the domain apex (@) if only the domain name was provided as --domain-alias + _sub_domain="@" + else + _sub_domain=$(echo "$domain" | cut -d . -f 1-$p) + fi + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_netlify_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + token_trimmed=$(echo "$NETLIFY_ACCESS_TOKEN" | tr -d '"') + + export _H1="Content-Type: application/json" + export _H2="Authorization: Bearer $token_trimmed" + + : >"$HTTP_HEADER" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$NETLIFY_URL$ep" "" "$m")" + else + response="$(_get "$NETLIFY_URL$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_nic.sh b/dnsapi/dns_nic.sh new file mode 100644 index 00000000..56170f87 --- /dev/null +++ b/dnsapi/dns_nic.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env sh + +# +#NIC_ClientID='0dc0xxxxxxxxxxxxxxxxxxxxxxxxce88' +#NIC_ClientSecret='3LTtxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxnuW8' +#NIC_Username="000000/NIC-D" +#NIC_Password="xxxxxxx" + +NIC_Api="https://api.nic.ru" + +dns_nic_add() { + fulldomain="${1}" + txtvalue="${2}" + + if ! _nic_get_authtoken save; then + _err "get NIC auth token failed" + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug _service "$_service" + + _info "Adding record" + if ! _nic_rest PUT "services/$_service/zones/$_domain/records" "$_sub_domainTXT$txtvalue"; then + _err "Add TXT record error" + return 1 + fi + + if ! _nic_rest POST "services/$_service/zones/$_domain/commit" ""; then + return 1 + fi + _info "Added, OK" +} + +dns_nic_rm() { + fulldomain="${1}" + txtvalue="${2}" + + if ! _nic_get_authtoken; then + _err "get NIC auth token failed" + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug _service "$_service" + + if ! _nic_rest GET "services/$_service/zones/$_domain/records"; then + _err "Get records error" + return 1 + fi + + _domain_id=$(printf "%s" "$response" | grep "$_sub_domain" | grep -- "$txtvalue" | sed -r "s/.*"; then + error=$(printf "%s" "$response" | grep "error code" | sed -r "s/.*(.*)<\/error>/\1/g") + _err "Error: $error" + return 1 + fi + + if ! _contains "$response" "success"; then + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_njalla.sh b/dnsapi/dns_njalla.sh new file mode 100644 index 00000000..e9243288 --- /dev/null +++ b/dnsapi/dns_njalla.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env sh + +# +#NJALLA_Token="sdfsdfsdfljlbjkljlkjsdfoiwje" + +NJALLA_Api="https://njal.la/api/1/" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_njalla_add() { + fulldomain=$1 + txtvalue=$2 + + NJALLA_Token="${NJALLA_Token:-$(_readaccountconf_mutable NJALLA_Token)}" + + if [ "$NJALLA_Token" ]; then + _saveaccountconf_mutable NJALLA_Token "$NJALLA_Token" + else + NJALLA_Token="" + _err "You didn't specify a Njalla api token yet." + 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 _njalla_rest "{\"method\":\"add-record\",\"params\":{\"domain\":\"$_domain\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":120}}"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + return 1 + +} + +#fulldomain txtvalue +dns_njalla_rm() { + fulldomain=$1 + txtvalue=$2 + + NJALLA_Token="${NJALLA_Token:-$(_readaccountconf_mutable NJALLA_Token)}" + + if [ "$NJALLA_Token" ]; then + _saveaccountconf_mutable NJALLA_Token "$NJALLA_Token" + else + NJALLA_Token="" + _err "You didn't specify a Njalla api token yet." + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting records for domain" + if ! _njalla_rest "{\"method\":\"list-records\",\"params\":{\"domain\":\"${_domain}\"}}"; then + return 1 + fi + + if ! echo "$response" | tr -d " " | grep "\"id\":" >/dev/null; then + _err "Error: $response" + return 1 + fi + + records=$(echo "$response" | _egrep_o "\"records\":\s?\[(.*)\]\}" | _egrep_o "\[.*\]" | _egrep_o "\{[^\{\}]*\"id\":[^\{\}]*\}") + count=$(echo "$records" | wc -l) + _debug count "$count" + + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + echo "$records" | while read -r record; do + record_name=$(echo "$record" | _egrep_o "\"name\":\s?\"[^\"]*\"" | cut -d : -f 2 | tr -d " " | tr -d \") + record_content=$(echo "$record" | _egrep_o "\"content\":\s?\"[^\"]*\"" | cut -d : -f 2 | tr -d " " | tr -d \") + record_id=$(echo "$record" | _egrep_o "\"id\":\s?[0-9]+" | cut -d : -f 2 | tr -d " " | tr -d \") + if [ "$_sub_domain" = "$record_name" ]; then + if [ "$txtvalue" = "$record_content" ]; then + _debug "record_id" "$record_id" + if ! _njalla_rest "{\"method\":\"remove-record\",\"params\":{\"domain\":\"${_domain}\",\"id\":${record_id}}}"; then + _err "Delete record error." + return 1 + fi + echo "$response" | tr -d " " | grep "\"result\"" >/dev/null + fi + fi + done + 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=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 ! _njalla_rest "{\"method\":\"get-domain\",\"params\":{\"domain\":\"${h}\"}}"; then + return 1 + fi + + if _contains "$response" "\"$h\""; then + _domain_returned=$(echo "$response" | _egrep_o "\{\"name\": *\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d " ") + if [ "$_domain_returned" ]; 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 +} + +_njalla_rest() { + data="$1" + + token_trimmed=$(echo "$NJALLA_Token" | tr -d '"') + + export _H1="Content-Type: application/json" + export _H2="Accept: application/json" + export _H3="Authorization: Njalla $token_trimmed" + + _debug data "$data" + response="$(_post "$data" "$NJALLA_Api" "" "POST")" + + if [ "$?" != "0" ]; then + _err "error $data" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_nm.sh b/dnsapi/dns_nm.sh new file mode 100644 index 00000000..4dfcc777 --- /dev/null +++ b/dnsapi/dns_nm.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env sh + +######################################################################## +# https://namemaster.de hook script for acme.sh +# +# Environment variables: +# +# - $NM_user (your namemaster.de API username) +# - $NM_sha256 (your namemaster.de API password_as_sha256hash) +# +# Author: Thilo Gass +# Git repo: https://github.com/ThiloGa/acme.sh + +#-- dns_nm_add() - Add TXT record -------------------------------------- +# Usage: dns_nm_add _acme-challenge.subdomain.domain.com "XyZ123..." + +namemaster_api="https://namemaster.de/api/api.php" + +dns_nm_add() { + fulldomain=$1 + txt_value=$2 + _info "Using DNS-01 namemaster hook" + + NM_user="${NM_user:-$(_readaccountconf_mutable NM_user)}" + NM_sha256="${NM_sha256:-$(_readaccountconf_mutable NM_sha256)}" + if [ -z "$NM_user" ] || [ -z "$NM_sha256" ]; then + NM_user="" + NM_sha256="" + _err "No auth details provided. Please set user credentials using the \$NM_user and \$NM_sha256 environment variables." + return 1 + fi + #save the api user and sha256 password to the account conf file. + _debug "Save user and hash" + _saveaccountconf_mutable NM_user "$NM_user" + _saveaccountconf_mutable NM_sha256 "$NM_sha256" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" "$fulldomain" + return 1 + fi + + _info "die Zone lautet:" "$zone" + + get="$namemaster_api?User=$NM_user&Password=$NM_sha256&Antwort=csv&Typ=ACME&zone=$zone&hostname=$fulldomain&TXT=$txt_value&Action=Auto&Lifetime=3600" + + if ! erg="$(_get "$get")"; then + _err "error Adding $fulldomain TXT: $txt_value" + return 1 + fi + + if _contains "$erg" "Success"; then + _info "Success, TXT Added, OK" + else + _err "error Adding $fulldomain TXT: $txt_value erg: $erg" + return 1 + fi + + _debug "ok Auto $fulldomain TXT: $txt_value erg: $erg" + return 0 +} + +dns_nm_rm() { + + fulldomain=$1 + txtvalue=$2 + _info "TXT enrty in $fulldomain is deleted automatically" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + +} + +_get_root() { + + domain=$1 + + get="$namemaster_api?User=$NM_user&Password=$NM_sha256&Typ=acme&hostname=$domain&Action=getzone&antwort=csv" + + if ! zone="$(_get "$get")"; then + _err "error getting Zone" + return 1 + else + if _contains "$zone" "hostname not found"; then + return 1 + fi + fi + +} diff --git a/dnsapi/dns_nsd.sh b/dnsapi/dns_nsd.sh new file mode 100644 index 00000000..0d29a485 --- /dev/null +++ b/dnsapi/dns_nsd.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env sh + +#Nsd_ZoneFile="/etc/nsd/zones/example.com.zone" +#Nsd_Command="sudo nsd-control reload" + +# args: fulldomain txtvalue +dns_nsd_add() { + fulldomain=$1 + txtvalue=$2 + ttlvalue=300 + + Nsd_ZoneFile="${Nsd_ZoneFile:-$(_readdomainconf Nsd_ZoneFile)}" + Nsd_Command="${Nsd_Command:-$(_readdomainconf Nsd_Command)}" + + # Arg checks + if [ -z "$Nsd_ZoneFile" ] || [ -z "$Nsd_Command" ]; then + Nsd_ZoneFile="" + Nsd_Command="" + _err "Specify ENV vars Nsd_ZoneFile and Nsd_Command" + return 1 + fi + + if [ ! -f "$Nsd_ZoneFile" ]; then + Nsd_ZoneFile="" + Nsd_Command="" + _err "No such file: $Nsd_ZoneFile" + return 1 + fi + + _savedomainconf Nsd_ZoneFile "$Nsd_ZoneFile" + _savedomainconf Nsd_Command "$Nsd_Command" + + echo "$fulldomain. $ttlvalue IN TXT \"$txtvalue\"" >>"$Nsd_ZoneFile" + _info "Added TXT record for $fulldomain" + _debug "Running $Nsd_Command" + if eval "$Nsd_Command"; then + _info "Successfully updated the zone" + return 0 + else + _err "Problem updating the zone" + return 1 + fi +} + +# args: fulldomain txtvalue +dns_nsd_rm() { + fulldomain=$1 + txtvalue=$2 + ttlvalue=300 + + Nsd_ZoneFile="${Nsd_ZoneFile:-$(_readdomainconf Nsd_ZoneFile)}" + Nsd_Command="${Nsd_Command:-$(_readdomainconf Nsd_Command)}" + + _sed_i "/$fulldomain. $ttlvalue IN TXT \"$txtvalue\"/d" "$Nsd_ZoneFile" + _info "Removed TXT record for $fulldomain" + _debug "Running $Nsd_Command" + if eval "$Nsd_Command"; then + _info "Successfully reloaded NSD " + return 0 + else + _err "Problem reloading NSD" + return 1 + fi +} diff --git a/dnsapi/dns_nsone.sh b/dnsapi/dns_nsone.sh index 00e186d2..9a998341 100644 --- a/dnsapi/dns_nsone.sh +++ b/dnsapi/dns_nsone.sh @@ -46,7 +46,7 @@ dns_nsone_add() { if [ "$count" = "0" ]; then _info "Adding record" - if _nsone_rest PUT "zones/$_domain/$fulldomain/TXT" "{\"answers\":[{\"answer\":[\"$txtvalue\"]}],\"type\":\"TXT\",\"domain\":\"$fulldomain\",\"zone\":\"$_domain\"}"; then + if _nsone_rest PUT "zones/$_domain/$fulldomain/TXT" "{\"answers\":[{\"answer\":[\"$txtvalue\"]}],\"type\":\"TXT\",\"domain\":\"$fulldomain\",\"zone\":\"$_domain\",\"ttl\":0}"; then if _contains "$response" "$fulldomain"; then _info "Added" #todo: check if the record takes effect @@ -62,7 +62,7 @@ dns_nsone_add() { prev_txt=$(printf "%s\n" "$response" | _egrep_o "\"domain\":\"$fulldomain\",\"short_answers\":\[\"[^,]*\]" | _head_n 1 | cut -d: -f3 | cut -d, -f1) _debug "prev_txt" "$prev_txt" - _nsone_rest POST "zones/$_domain/$fulldomain/TXT" "{\"answers\": [{\"answer\": [\"$txtvalue\"]},{\"answer\": $prev_txt}],\"type\": \"TXT\",\"domain\":\"$fulldomain\",\"zone\": \"$_domain\"}" + _nsone_rest POST "zones/$_domain/$fulldomain/TXT" "{\"answers\": [{\"answer\": [\"$txtvalue\"]},{\"answer\": $prev_txt}],\"type\": \"TXT\",\"domain\":\"$fulldomain\",\"zone\": \"$_domain\",\"ttl\":0}" if [ "$?" = "0" ] && _contains "$response" "$fulldomain"; then _info "Updated!" #todo: check if the record takes effect diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index 555f4d29..cd4b7140 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -6,19 +6,39 @@ dns_nsupdate_add() { fulldomain=$1 txtvalue=$2 + NSUPDATE_SERVER="${NSUPDATE_SERVER:-$(_readaccountconf_mutable NSUPDATE_SERVER)}" + NSUPDATE_SERVER_PORT="${NSUPDATE_SERVER_PORT:-$(_readaccountconf_mutable NSUPDATE_SERVER_PORT)}" + NSUPDATE_KEY="${NSUPDATE_KEY:-$(_readaccountconf_mutable NSUPDATE_KEY)}" + NSUPDATE_ZONE="${NSUPDATE_ZONE:-$(_readaccountconf_mutable NSUPDATE_ZONE)}" + _checkKeyFile || return 1 + + # save the dns server and key to the account conf file. + _saveaccountconf_mutable NSUPDATE_SERVER "${NSUPDATE_SERVER}" + _saveaccountconf_mutable NSUPDATE_SERVER_PORT "${NSUPDATE_SERVER_PORT}" + _saveaccountconf_mutable NSUPDATE_KEY "${NSUPDATE_KEY}" + _saveaccountconf_mutable NSUPDATE_ZONE "${NSUPDATE_ZONE}" + [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost" [ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53 - # save the dns server and key to the account conf file. - _saveaccountconf NSUPDATE_SERVER "${NSUPDATE_SERVER}" - _saveaccountconf NSUPDATE_SERVER_PORT "${NSUPDATE_SERVER_PORT}" - _saveaccountconf NSUPDATE_KEY "${NSUPDATE_KEY}" + _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\"" - nsupdate -k "${NSUPDATE_KEY}" < + +NW_API_VERSION="0" + +# dns_nw_add() - Add TXT record +# Usage: dns_nw_add _acme-challenge.subdomain.domain.com "XyZ123..." +dns_nw_add() { + host="${1}" + txtvalue="${2}" + + _debug host "${host}" + _debug txtvalue "${txtvalue}" + + if ! _check_nw_api_creds; then + return 1 + fi + + _info "Using NocWorx (${NW_API_ENDPOINT})" + _debug "Calling: dns_nw_add() '${host}' '${txtvalue}'" + + _debug "Detecting root zone" + if ! _get_root "${host}"; then + _err "Zone for domain does not exist." + return 1 + fi + _debug _zone_id "${_zone_id}" + _debug _sub_domain "${_sub_domain}" + _debug _domain "${_domain}" + + _post_data="{\"zone_id\": \"${_zone_id}\", \"type\": \"TXT\", \"host\": \"${host}\", \"target\": \"${txtvalue}\", \"ttl\": \"300\"}" + + if _rest POST "dns-record" "${_post_data}" && [ -n "${response}" ]; then + _record_id=$(printf "%s\n" "${response}" | _egrep_o "\"record_id\": *[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1) + _debug _record_id "${_record_id}" + + if [ -z "$_record_id" ]; then + _err "Error adding the TXT record." + return 1 + fi + + _info "TXT record successfully added." + return 0 + fi + + return 1 +} + +# dns_nw_rm() - Remove TXT record +# Usage: dns_nw_rm _acme-challenge.subdomain.domain.com "XyZ123..." +dns_nw_rm() { + host="${1}" + txtvalue="${2}" + + _debug host "${host}" + _debug txtvalue "${txtvalue}" + + if ! _check_nw_api_creds; then + return 1 + fi + + _info "Using NocWorx (${NW_API_ENDPOINT})" + _debug "Calling: dns_nw_rm() '${host}'" + + _debug "Detecting root zone" + if ! _get_root "${host}"; then + _err "Zone for domain does not exist." + return 1 + fi + _debug _zone_id "${_zone_id}" + _debug _sub_domain "${_sub_domain}" + _debug _domain "${_domain}" + + _parameters="?zone_id=${_zone_id}" + + if _rest GET "dns-record" "${_parameters}" && [ -n "${response}" ]; then + response="$(echo "${response}" | tr -d "\n" | sed 's/^\[\(.*\)\]$/\1/' | sed -e 's/{"record_id":/|"record_id":/g' | sed 's/|/&{/g' | tr "|" "\n")" + _debug response "${response}" + + record="$(echo "${response}" | _egrep_o "{.*\"host\": *\"${_sub_domain}\", *\"target\": *\"${txtvalue}\".*}")" + _debug record "${record}" + + if [ "${record}" ]; then + _record_id=$(printf "%s\n" "${record}" | _egrep_o "\"record_id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + if [ "${_record_id}" ]; then + _debug _record_id "${_record_id}" + + _rest DELETE "dns-record/${_record_id}" + + _info "TXT record successfully deleted." + return 0 + fi + + return 1 + fi + + return 0 + fi + + return 1 +} + +_check_nw_api_creds() { + NW_API_TOKEN="${NW_API_TOKEN:-$(_readaccountconf_mutable NW_API_TOKEN)}" + NW_API_ENDPOINT="${NW_API_ENDPOINT:-$(_readaccountconf_mutable NW_API_ENDPOINT)}" + + if [ -z "${NW_API_ENDPOINT}" ]; then + NW_API_ENDPOINT="https://portal.nexcess.net" + fi + + if [ -z "${NW_API_TOKEN}" ]; then + _err "You have not defined your NW_API_TOKEN." + _err "Please create your token and try again." + _err "If you need to generate a new token, please visit one of the following URLs:" + _err " - https://portal.nexcess.net/api-token" + _err " - https://core.thermo.io/api-token" + _err " - https://my.futurehosting.com/api-token" + + return 1 + fi + + _saveaccountconf_mutable NW_API_TOKEN "${NW_API_TOKEN}" + _saveaccountconf_mutable NW_API_ENDPOINT "${NW_API_ENDPOINT}" +} + +_get_root() { + domain="${1}" + i=2 + p=1 + + if _rest GET "dns-zone"; then + response="$(echo "${response}" | tr -d "\n" | sed 's/^\[\(.*\)\]$/\1/' | sed -e 's/{"zone_id":/|"zone_id":/g' | sed 's/|/&{/g' | tr "|" "\n")" + + _debug response "${response}" + while true; do + h=$(printf "%s" "${domain}" | cut -d . -f $i-100) + _debug h "${h}" + if [ -z "${h}" ]; then + #not valid + return 1 + fi + + hostedzone="$(echo "${response}" | _egrep_o "{.*\"domain\": *\"${h}\".*}")" + if [ "${hostedzone}" ]; then + _zone_id=$(printf "%s\n" "${hostedzone}" | _egrep_o "\"zone_id\": *[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + if [ "${_zone_id}" ]; then + _sub_domain=$(printf "%s" "${domain}" | cut -d . -f 1-${p}) + _domain="${h}" + return 0 + fi + return 1 + fi + p=$i + i=$(_math "${i}" + 1) + done + fi + return 1 +} + +_rest() { + method="${1}" + ep="/${2}" + data="${3}" + + _debug method "${method}" + _debug ep "${ep}" + + export _H1="Accept: application/json" + export _H2="Content-Type: application/json" + export _H3="Api-Version: ${NW_API_VERSION}" + export _H4="User-Agent: NW-ACME-CLIENT" + export _H5="Authorization: Bearer ${NW_API_TOKEN}" + + if [ "${method}" != "GET" ]; then + _debug data "${data}" + response="$(_post "${data}" "${NW_API_ENDPOINT}${ep}" "" "${method}")" + else + response="$(_get "${NW_API_ENDPOINT}${ep}${data}")" + fi + + if [ "${?}" != "0" ]; then + _err "error ${ep}" + return 1 + fi + _debug2 response "${response}" + return 0 +} diff --git a/dnsapi/dns_oci.sh b/dnsapi/dns_oci.sh new file mode 100644 index 00000000..3b81143f --- /dev/null +++ b/dnsapi/dns_oci.sh @@ -0,0 +1,325 @@ +#!/usr/bin/env sh +# +# Acme.sh DNS API plugin for Oracle Cloud Infrastructure +# Copyright (c) 2021, Oracle and/or its affiliates +# +# The plugin will automatically use the default profile from an OCI SDK and CLI +# configuration file, if it exists. +# +# Alternatively, set the following environment variables: +# - OCI_CLI_TENANCY : OCID of tenancy that contains the target DNS zone +# - OCI_CLI_USER : OCID of user with permission to add/remove records from zones +# - OCI_CLI_REGION : Should point to the tenancy home region +# +# One of the following two variables is required: +# - OCI_CLI_KEY_FILE: Path to private API signing key file in PEM format; or +# - OCI_CLI_KEY : The private API signing key in PEM format +# +# NOTE: using an encrypted private key that needs a passphrase is not supported. +# + +dns_oci_add() { + _fqdn="$1" + _rdata="$2" + + if _get_oci_zone; then + + _add_record_body="{\"items\":[{\"domain\":\"${_sub_domain}.${_domain}\",\"rdata\":\"$_rdata\",\"rtype\":\"TXT\",\"ttl\": 30,\"operation\":\"ADD\"}]}" + response=$(_signed_request "PATCH" "/20180115/zones/${_domain}/records" "$_add_record_body") + if [ "$response" ]; then + _info "Success: added TXT record for ${_sub_domain}.${_domain}." + else + _err "Error: failed to add TXT record for ${_sub_domain}.${_domain}." + _err "Check that the user has permission to add records to this zone." + return 1 + fi + + else + return 1 + fi + +} + +dns_oci_rm() { + _fqdn="$1" + _rdata="$2" + + if _get_oci_zone; then + + _remove_record_body="{\"items\":[{\"domain\":\"${_sub_domain}.${_domain}\",\"rdata\":\"$_rdata\",\"rtype\":\"TXT\",\"operation\":\"REMOVE\"}]}" + response=$(_signed_request "PATCH" "/20180115/zones/${_domain}/records" "$_remove_record_body") + if [ "$response" ]; then + _info "Success: removed TXT record for ${_sub_domain}.${_domain}." + else + _err "Error: failed to remove TXT record for ${_sub_domain}.${_domain}." + _err "Check that the user has permission to remove records from this zone." + return 1 + fi + + else + return 1 + fi + +} + +#################### Private functions below ################################## +_get_oci_zone() { + + if ! _oci_config; then + return 1 + fi + + if ! _get_zone "$_fqdn"; then + _err "Error: DNS Zone not found for $_fqdn in $OCI_CLI_TENANCY" + return 1 + fi + + return 0 + +} + +_oci_config() { + + _DEFAULT_OCI_CLI_CONFIG_FILE="$HOME/.oci/config" + OCI_CLI_CONFIG_FILE="${OCI_CLI_CONFIG_FILE:-$(_readaccountconf_mutable OCI_CLI_CONFIG_FILE)}" + + if [ -z "$OCI_CLI_CONFIG_FILE" ]; then + OCI_CLI_CONFIG_FILE="$_DEFAULT_OCI_CLI_CONFIG_FILE" + fi + + if [ "$_DEFAULT_OCI_CLI_CONFIG_FILE" != "$OCI_CLI_CONFIG_FILE" ]; then + _saveaccountconf_mutable OCI_CLI_CONFIG_FILE "$OCI_CLI_CONFIG_FILE" + else + _clearaccountconf_mutable OCI_CLI_CONFIG_FILE + fi + + _DEFAULT_OCI_CLI_PROFILE="DEFAULT" + OCI_CLI_PROFILE="${OCI_CLI_PROFILE:-$(_readaccountconf_mutable OCI_CLI_PROFILE)}" + if [ "$_DEFAULT_OCI_CLI_PROFILE" != "$OCI_CLI_PROFILE" ]; then + _saveaccountconf_mutable OCI_CLI_PROFILE "$OCI_CLI_PROFILE" + else + OCI_CLI_PROFILE="$_DEFAULT_OCI_CLI_PROFILE" + _clearaccountconf_mutable OCI_CLI_PROFILE + fi + + OCI_CLI_TENANCY="${OCI_CLI_TENANCY:-$(_readaccountconf_mutable OCI_CLI_TENANCY)}" + if [ "$OCI_CLI_TENANCY" ]; then + _saveaccountconf_mutable OCI_CLI_TENANCY "$OCI_CLI_TENANCY" + elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then + _debug "Reading OCI_CLI_TENANCY value from: $OCI_CLI_CONFIG_FILE" + OCI_CLI_TENANCY="${OCI_CLI_TENANCY:-$(_readini "$OCI_CLI_CONFIG_FILE" tenancy "$OCI_CLI_PROFILE")}" + fi + + if [ -z "$OCI_CLI_TENANCY" ]; then + _err "Error: unable to read OCI_CLI_TENANCY from config file or environment variable." + return 1 + fi + + OCI_CLI_USER="${OCI_CLI_USER:-$(_readaccountconf_mutable OCI_CLI_USER)}" + if [ "$OCI_CLI_USER" ]; then + _saveaccountconf_mutable OCI_CLI_USER "$OCI_CLI_USER" + elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then + _debug "Reading OCI_CLI_USER value from: $OCI_CLI_CONFIG_FILE" + OCI_CLI_USER="${OCI_CLI_USER:-$(_readini "$OCI_CLI_CONFIG_FILE" user "$OCI_CLI_PROFILE")}" + fi + if [ -z "$OCI_CLI_USER" ]; then + _err "Error: unable to read OCI_CLI_USER from config file or environment variable." + return 1 + fi + + OCI_CLI_REGION="${OCI_CLI_REGION:-$(_readaccountconf_mutable OCI_CLI_REGION)}" + if [ "$OCI_CLI_REGION" ]; then + _saveaccountconf_mutable OCI_CLI_REGION "$OCI_CLI_REGION" + elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then + _debug "Reading OCI_CLI_REGION value from: $OCI_CLI_CONFIG_FILE" + OCI_CLI_REGION="${OCI_CLI_REGION:-$(_readini "$OCI_CLI_CONFIG_FILE" region "$OCI_CLI_PROFILE")}" + fi + if [ -z "$OCI_CLI_REGION" ]; then + _err "Error: unable to read OCI_CLI_REGION from config file or environment variable." + return 1 + fi + + OCI_CLI_KEY="${OCI_CLI_KEY:-$(_readaccountconf_mutable OCI_CLI_KEY)}" + if [ -z "$OCI_CLI_KEY" ]; then + _clearaccountconf_mutable OCI_CLI_KEY + OCI_CLI_KEY_FILE="${OCI_CLI_KEY_FILE:-$(_readini "$OCI_CLI_CONFIG_FILE" key_file "$OCI_CLI_PROFILE")}" + if [ "$OCI_CLI_KEY_FILE" ] && [ -f "$OCI_CLI_KEY_FILE" ]; then + _debug "Reading OCI_CLI_KEY value from: $OCI_CLI_KEY_FILE" + OCI_CLI_KEY=$(_base64 <"$OCI_CLI_KEY_FILE") + _saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY" + fi + else + _saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY" + fi + + if [ -z "$OCI_CLI_KEY_FILE" ] && [ -z "$OCI_CLI_KEY" ]; then + _err "Error: unable to find key file path in OCI config file or OCI_CLI_KEY_FILE." + _err "Error: unable to load private API signing key from OCI_CLI_KEY." + return 1 + fi + + if [ "$(printf "%s\n" "$OCI_CLI_KEY" | wc -l)" -eq 1 ]; then + OCI_CLI_KEY=$(printf "%s" "$OCI_CLI_KEY" | _dbase64) + fi + + return 0 + +} + +# _get_zone(): retrieves the Zone name and OCID +# +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_ociid=ocid1.dns-zone.oc1.. +_get_zone() { + 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 + + _domain_id=$(_signed_request "GET" "/20180115/zones/$h" "" "id") + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done + return 1 + +} + +#Usage: privatekey +#Output MD5 fingerprint +_fingerprint() { + + pkey="$1" + if [ -z "$pkey" ]; then + _usage "Usage: _fingerprint privkey" + return 1 + fi + + printf "%s" "$pkey" | ${ACME_OPENSSL_BIN:-openssl} rsa -pubout -outform DER 2>/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" + # shellcheck disable=SC2090 + 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 new file mode 100644 index 00000000..1565b767 --- /dev/null +++ b/dnsapi/dns_one.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env sh +# one.com ui wrapper for acme.sh + +# +# export ONECOM_User="username" +# export ONECOM_Password="password" + +dns_one_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _dns_one_login; then + _err "login failed" + return 1 + fi + + _debug "detect the root domain" + if ! _get_root "$fulldomain"; then + _err "root domain not found" + return 1 + fi + + subdomain="${_sub_domain}" + maindomain=${_domain} + + _debug subdomain "$subdomain" + _debug maindomain "$maindomain" + + #Check if the TXT exists + _dns_one_getrecord "TXT" "$subdomain" "$txtvalue" + if [ -n "$id" ]; then + _info "$(__green "Txt record with the same value found. Skip adding.")" + return 0 + fi + + _dns_one_addrecord "TXT" "$subdomain" "$txtvalue" + if [ -z "$id" ]; then + _err "Add TXT record error." + return 1 + else + _info "$(__green "Added, OK ($id)")" + return 0 + fi +} + +dns_one_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _dns_one_login; then + _err "login failed" + return 1 + fi + + _debug "detect the root domain" + if ! _get_root "$fulldomain"; then + _err "root domain not found" + return 1 + fi + + subdomain="${_sub_domain}" + maindomain=${_domain} + + _debug subdomain "$subdomain" + _debug maindomain "$maindomain" + + #Check if the TXT exists + _dns_one_getrecord "TXT" "$subdomain" "$txtvalue" + if [ -z "$id" ]; then + _err "Txt record not found." + return 1 + fi + + # delete entry + if _dns_one_delrecord "$id"; then + _info "$(__green Removed, OK)" + return 0 + else + _err "Removing txt record error." + return 1 + fi +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain="$1" + i=1 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + + if [ -z "$h" ]; then + #not valid + return 1 + fi + + response="$(_get "https://www.one.com/admin/api/domains/$h/dns/custom_records")" + + if ! _contains "$response" "CRMRST_000302"; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + _err "Unable to parse this domain" + return 1 +} + +_dns_one_login() { + + # get credentials + ONECOM_User="${ONECOM_User:-$(_readaccountconf_mutable ONECOM_User)}" + ONECOM_Password="${ONECOM_Password:-$(_readaccountconf_mutable ONECOM_Password)}" + if [ -z "$ONECOM_User" ] || [ -z "$ONECOM_Password" ]; then + ONECOM_User="" + ONECOM_Password="" + _err "You didn't specify a one.com username and password yet." + _err "Please create the key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable ONECOM_User "$ONECOM_User" + _saveaccountconf_mutable ONECOM_Password "$ONECOM_Password" + + # Login with user and password + postdata="loginDomain=true" + postdata="$postdata&displayUsername=$ONECOM_User" + postdata="$postdata&username=$ONECOM_User" + postdata="$postdata&targetDomain=" + postdata="$postdata&password1=$ONECOM_Password" + postdata="$postdata&loginTarget=" + #_debug postdata "$postdata" + + response="$(_post "$postdata" "https://www.one.com/admin/login.do" "" "POST" "application/x-www-form-urlencoded")" + #_debug response "$response" + + # Get SessionID + JSESSIONID="$(grep "OneSIDCrmAdmin" "$HTTP_HEADER" | grep "^[Ss]et-[Cc]ookie:" | _head_n 1 | _egrep_o 'OneSIDCrmAdmin=[^;]*;' | tr -d ';')" + _debug jsessionid "$JSESSIONID" + + if [ -z "$JSESSIONID" ]; then + _err "error sessionid cookie not found" + return 1 + fi + + export _H1="Cookie: ${JSESSIONID}" + + return 0 +} + +_dns_one_getrecord() { + type="$1" + name="$2" + value="$3" + if [ -z "$type" ]; then + type="TXT" + fi + if [ -z "$name" ]; then + _err "Record name is empty." + return 1 + fi + + response="$(_get "https://www.one.com/admin/api/domains/$maindomain/dns/custom_records")" + response="$(echo "$response" | _normalizeJson)" + _debug response "$response" + + if [ -z "${value}" ]; then + id=$(printf -- "%s" "$response" | sed -n "s/.*{\"type\":\"dns_custom_records\",\"id\":\"\([^\"]*\)\",\"attributes\":{\"prefix\":\"${name}\",\"type\":\"${type}\",\"content\":\"[^\"]*\",\"priority\":0,\"ttl\":600}.*/\1/p") + response=$(printf -- "%s" "$response" | sed -n "s/.*{\"type\":\"dns_custom_records\",\"id\":\"[^\"]*\",\"attributes\":{\"prefix\":\"${name}\",\"type\":\"${type}\",\"content\":\"\([^\"]*\)\",\"priority\":0,\"ttl\":600}.*/\1/p") + else + id=$(printf -- "%s" "$response" | sed -n "s/.*{\"type\":\"dns_custom_records\",\"id\":\"\([^\"]*\)\",\"attributes\":{\"prefix\":\"${name}\",\"type\":\"${type}\",\"content\":\"${value}\",\"priority\":0,\"ttl\":600}.*/\1/p") + fi + if [ -z "$id" ]; then + return 1 + fi + return 0 +} + +_dns_one_addrecord() { + type="$1" + name="$2" + value="$3" + if [ -z "$type" ]; then + type="TXT" + fi + if [ -z "$name" ]; then + _err "Record name is empty." + return 1 + fi + + postdata="{\"type\":\"dns_custom_records\",\"attributes\":{\"priority\":0,\"ttl\":600,\"type\":\"${type}\",\"prefix\":\"${name}\",\"content\":\"${value}\"}}" + _debug postdata "$postdata" + response="$(_post "$postdata" "https://www.one.com/admin/api/domains/$maindomain/dns/custom_records" "" "POST" "application/json")" + response="$(echo "$response" | _normalizeJson)" + _debug response "$response" + + id=$(echo "$response" | sed -n "s/{\"result\":{\"data\":{\"type\":\"dns_custom_records\",\"id\":\"\([^\"]*\)\",\"attributes\":{\"prefix\":\"$subdomain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"priority\":0,\"ttl\":600}}},\"metadata\":null}/\1/p") + + if [ -z "$id" ]; then + return 1 + else + return 0 + fi +} + +_dns_one_delrecord() { + id="$1" + if [ -z "$id" ]; then + return 1 + fi + + response="$(_post "" "https://www.one.com/admin/api/domains/$maindomain/dns/custom_records/$id" "" "DELETE" "application/json")" + response="$(echo "$response" | _normalizeJson)" + _debug response "$response" + + if [ "$response" = '{"result":null,"metadata":null}' ]; then + return 0 + else + return 1 + fi +} diff --git a/dnsapi/dns_online.sh b/dnsapi/dns_online.sh new file mode 100755 index 00000000..9158c268 --- /dev/null +++ b/dnsapi/dns_online.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env sh + +# Online API +# https://console.online.net/en/api/ +# +# Requires Online API key set in ONLINE_API_KEY + +######## Public functions ##################### + +ONLINE_API="https://api.online.net/api/v1" + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_online_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _online_check_config; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug _real_dns_version "$_real_dns_version" + + _info "Creating temporary zone version" + _online_create_temporary_zone_version + _info "Enabling temporary zone version" + _online_enable_zone "$_temporary_dns_version" + + _info "Adding record" + _online_create_TXT_record "$_real_dns_version" "$_sub_domain" "$txtvalue" + _info "Disabling temporary version" + _online_enable_zone "$_real_dns_version" + _info "Destroying temporary version" + _online_destroy_zone "$_temporary_dns_version" + + _info "Record added." + return 0 +} + +#fulldomain +dns_online_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _online_check_config; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug _real_dns_version "$_real_dns_version" + + _debug "Getting txt records" + if ! _online_rest GET "domain/$_domain/version/active"; then + return 1 + fi + + rid=$(echo "$response" | _egrep_o "\"id\":[0-9]+,\"name\":\"$_sub_domain\",\"data\":\"\\\u0022$txtvalue\\\u0022\"" | cut -d ':' -f 2 | cut -d ',' -f 1) + _debug rid "$rid" + if [ -z "$rid" ]; then + return 1 + fi + + _info "Creating temporary zone version" + _online_create_temporary_zone_version + _info "Enabling temporary zone version" + _online_enable_zone "$_temporary_dns_version" + + _info "Removing DNS record" + _online_rest DELETE "domain/$_domain/version/$_real_dns_version/zone/$rid" + _info "Disabling temporary version" + _online_enable_zone "$_real_dns_version" + _info "Destroying temporary version" + _online_destroy_zone "$_temporary_dns_version" + + return 0 +} + +#################### Private functions below ################################## + +_online_check_config() { + ONLINE_API_KEY="${ONLINE_API_KEY:-$(_readaccountconf_mutable ONLINE_API_KEY)}" + if [ -z "$ONLINE_API_KEY" ]; then + _err "No API key specified for Online API." + _err "Create your key and export it as ONLINE_API_KEY" + return 1 + fi + if ! _online_rest GET "domain/"; then + _err "Invalid API key specified for Online API." + return 1 + fi + + _saveaccountconf_mutable ONLINE_API_KEY "$ONLINE_API_KEY" + + return 0 +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + _online_rest GET "domain/$h/version/active" + + if ! _contains "$response" "Domain not found" >/dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + _real_dns_version=$(echo "$response" | _egrep_o '"uuid_ref":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + _err "Unable to retrive DNS zone matching this domain" + return 1 +} + +# this function create a temporary zone version +# as online.net does not allow updating an active version +_online_create_temporary_zone_version() { + + _online_rest POST "domain/$_domain/version" "name=acme.sh" + if [ "$?" != "0" ]; then + return 1 + fi + + _temporary_dns_version=$(echo "$response" | _egrep_o '"uuid_ref":.*' | cut -d ':' -f 2 | cut -d '"' -f 2) + + # Creating a dummy record in this temporary version, because online.net doesn't accept enabling an empty version + _online_create_TXT_record "$_temporary_dns_version" "dummy.acme.sh" "dummy" + + return 0 +} + +_online_destroy_zone() { + version_id=$1 + _online_rest DELETE "domain/$_domain/version/$version_id" + + if [ "$?" != "0" ]; then + return 1 + fi + return 0 +} + +_online_enable_zone() { + version_id=$1 + _online_rest PATCH "domain/$_domain/version/$version_id/enable" + + if [ "$?" != "0" ]; then + return 1 + fi + return 0 +} + +_online_create_TXT_record() { + version=$1 + txt_name=$2 + txt_value=$3 + + _online_rest POST "domain/$_domain/version/$version/zone" "type=TXT&name=$txt_name&data=%22$txt_value%22&ttl=60&priority=0" + + # Note : the normal, expected response SHOULD be "Unknown method". + # this happens because the API HTTP response contains a Location: header, that redirect + # to an unknown online.net endpoint. + if [ "$?" != "0" ] || _contains "$response" "Unknown method" || _contains "$response" "\$ref"; then + return 0 + else + _err "error $response" + return 1 + fi +} + +_online_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + _online_url="$ONLINE_API/$ep" + _debug2 _online_url "$_online_url" + export _H1="Authorization: Bearer $ONLINE_API_KEY" + export _H2="X-Pretty-JSON: 1" + if [ "$data" ] || [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$_online_url" "" "$m")" + else + response="$(_get "$_online_url")" + fi + if [ "$?" != "0" ] || _contains "$response" "invalid_grant" || _contains "$response" "Method not allowed"; then + _err "error $response" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_openprovider.sh b/dnsapi/dns_openprovider.sh new file mode 100755 index 00000000..0a9e5ade --- /dev/null +++ b/dnsapi/dns_openprovider.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env sh + +# This is the OpenProvider API wrapper for acme.sh +# +# Author: Sylvia van Os +# Report Bugs here: https://github.com/acmesh-official/acme.sh/issues/2104 +# +# export OPENPROVIDER_USER="username" +# export OPENPROVIDER_PASSWORDHASH="hashed_password" +# +# Usage: +# acme.sh --issue --dns dns_openprovider -d example.com + +OPENPROVIDER_API="https://api.openprovider.eu/" +#OPENPROVIDER_API="https://api.cte.openprovider.eu/" # Test API + +######## Public functions ##################### + +#Usage: dns_openprovider_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_openprovider_add() { + fulldomain="$1" + txtvalue="$2" + + OPENPROVIDER_USER="${OPENPROVIDER_USER:-$(_readaccountconf_mutable OPENPROVIDER_USER)}" + OPENPROVIDER_PASSWORDHASH="${OPENPROVIDER_PASSWORDHASH:-$(_readaccountconf_mutable OPENPROVIDER_PASSWORDHASH)}" + + if [ -z "$OPENPROVIDER_USER" ] || [ -z "$OPENPROVIDER_PASSWORDHASH" ]; then + _err "You didn't specify the openprovider user and/or password hash." + return 1 + fi + + # save the username and password to the account conf file. + _saveaccountconf_mutable OPENPROVIDER_USER "$OPENPROVIDER_USER" + _saveaccountconf_mutable OPENPROVIDER_PASSWORDHASH "$OPENPROVIDER_PASSWORDHASH" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _domain_name "$_domain_name" + _debug _domain_extension "$_domain_extension" + + _debug "Getting current records" + existing_items="" + results_retrieved=0 + while true; do + _openprovider_request "$(printf '%s.%s%s' "$_domain_name" "$_domain_extension" "$results_retrieved")" + + items="$response" + while true; do + item="$(echo "$items" | _egrep_o '.*<\/openXML>' | sed -n 's/.*\(.*<\/item>\).*/\1/p')" + _debug existing_items "$existing_items" + _debug results_retrieved "$results_retrieved" + _debug item "$item" + + if [ -z "$item" ]; then + break + fi + + tmpitem="$(echo "$item" | sed 's/\*/\\*/g')" + items="$(echo "$items" | sed "s|${tmpitem}||")" + + results_retrieved="$(_math "$results_retrieved" + 1)" + new_item="$(echo "$item" | sed -n 's/.*.*\(\(.*\)\.'"$_domain_name"'\.'"$_domain_extension"'<\/name>.*\(.*<\/type>\).*\(.*<\/value>\).*\(.*<\/prio>\).*\(.*<\/ttl>\)\).*<\/item>.*/\2<\/name>\3\4\5\6<\/item>/p')" + if [ -z "$new_item" ]; then + # Domain apex + new_item="$(echo "$item" | sed -n 's/.*.*\(\(.*\)'"$_domain_name"'\.'"$_domain_extension"'<\/name>.*\(.*<\/type>\).*\(.*<\/value>\).*\(.*<\/prio>\).*\(.*<\/ttl>\)\).*<\/item>.*/\2<\/name>\3\4\5\6<\/item>/p')" + fi + + if [ -z "$(echo "$new_item" | _egrep_o ".*(A|AAAA|CNAME|MX|SPF|SRV|TXT|TLSA|SSHFP|CAA|NS)<\/type>.*")" ]; then + _debug "not an allowed record type, skipping" "$new_item" + continue + fi + + existing_items="$existing_items$new_item" + done + + total="$(echo "$response" | _egrep_o '.*?<\/total>' | sed -n 's/.*\(.*\)<\/total>.*/\1/p')" + + _debug total "$total" + if [ "$results_retrieved" -eq "$total" ]; then + break + fi + done + + _debug "Creating acme record" + acme_record="$(echo "$fulldomain" | sed -e "s/.$_domain_name.$_domain_extension$//")" + _openprovider_request "$(printf '%s%smaster%s%sTXT%s600' "$_domain_name" "$_domain_extension" "$existing_items" "$acme_record" "$txtvalue")" + + return 0 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_openprovider_rm() { + fulldomain="$1" + txtvalue="$2" + + OPENPROVIDER_USER="${OPENPROVIDER_USER:-$(_readaccountconf_mutable OPENPROVIDER_USER)}" + OPENPROVIDER_PASSWORDHASH="${OPENPROVIDER_PASSWORDHASH:-$(_readaccountconf_mutable OPENPROVIDER_PASSWORDHASH)}" + + if [ -z "$OPENPROVIDER_USER" ] || [ -z "$OPENPROVIDER_PASSWORDHASH" ]; then + _err "You didn't specify the openprovider user and/or password hash." + return 1 + fi + + # save the username and password to the account conf file. + _saveaccountconf_mutable OPENPROVIDER_USER "$OPENPROVIDER_USER" + _saveaccountconf_mutable OPENPROVIDER_PASSWORDHASH "$OPENPROVIDER_PASSWORDHASH" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _domain_name "$_domain_name" + _debug _domain_extension "$_domain_extension" + + _debug "Getting current records" + existing_items="" + results_retrieved=0 + while true; do + _openprovider_request "$(printf '%s.%s%s' "$_domain_name" "$_domain_extension" "$results_retrieved")" + + # Remove acme records from items + items="$response" + while true; do + item="$(echo "$items" | _egrep_o '.*<\/openXML>' | sed -n 's/.*\(.*<\/item>\).*/\1/p')" + _debug existing_items "$existing_items" + _debug results_retrieved "$results_retrieved" + _debug item "$item" + + if [ -z "$item" ]; then + break + fi + + tmpitem="$(echo "$item" | sed 's/\*/\\*/g')" + items="$(echo "$items" | sed "s|${tmpitem}||")" + + results_retrieved="$(_math "$results_retrieved" + 1)" + if ! echo "$item" | grep -v "$fulldomain"; then + _debug "acme record, skipping" "$item" + continue + fi + + new_item="$(echo "$item" | sed -n 's/.*.*\(\(.*\)\.'"$_domain_name"'\.'"$_domain_extension"'<\/name>.*\(.*<\/type>\).*\(.*<\/value>\).*\(.*<\/prio>\).*\(.*<\/ttl>\)\).*<\/item>.*/\2<\/name>\3\4\5\6<\/item>/p')" + + if [ -z "$new_item" ]; then + # domain apex + new_item="$(echo "$item" | sed -n 's/.*.*\(\(.*\)'"$_domain_name"'\.'"$_domain_extension"'<\/name>.*\(.*<\/type>\).*\(.*<\/value>\).*\(.*<\/prio>\).*\(.*<\/ttl>\)\).*<\/item>.*/\2<\/name>\3\4\5\6<\/item>/p')" + fi + + if [ -z "$(echo "$new_item" | _egrep_o ".*(A|AAAA|CNAME|MX|SPF|SRV|TXT|TLSA|SSHFP|CAA|NS)<\/type>.*")" ]; then + _debug "not an allowed record type, skipping" "$new_item" + continue + fi + + existing_items="$existing_items$new_item" + done + + total="$(echo "$response" | _egrep_o '.*?<\/total>' | sed -n 's/.*\(.*\)<\/total>.*/\1/p')" + + _debug total "$total" + + if [ "$results_retrieved" -eq "$total" ]; then + break + fi + done + + _debug "Removing acme record" + _openprovider_request "$(printf '%s%smaster%s' "$_domain_name" "$_domain_extension" "$existing_items")" + + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _domain_name=domain +# _domain_extension=com +_get_root() { + domain=$1 + i=2 + + results_retrieved=0 + while true; do + h=$(echo "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + _openprovider_request "$(printf '%s%s' "$(echo "$h" | cut -d . -f 1)" "$results_retrieved")" + + items="$response" + while true; do + item="$(echo "$items" | _egrep_o '.*<\/openXML>' | sed -n 's/.*\(.*<\/domain>\).*/\1/p')" + _debug existing_items "$existing_items" + _debug results_retrieved "$results_retrieved" + _debug item "$item" + + if [ -z "$item" ]; then + break + fi + + tmpitem="$(echo "$item" | sed 's/\*/\\*/g')" + items="$(echo "$items" | sed "s|${tmpitem}||")" + + results_retrieved="$(_math "$results_retrieved" + 1)" + + _domain_name="$(echo "$item" | sed -n 's/.*.*\(.*\)<\/name>.*<\/domain>.*/\1/p')" + _domain_extension="$(echo "$item" | sed -n 's/.*.*\(.*\)<\/extension>.*<\/domain>.*/\1/p')" + _debug _domain_name "$_domain_name" + _debug _domain_extension "$_domain_extension" + if [ "$_domain_name.$_domain_extension" = "$h" ]; then + return 0 + fi + done + + total="$(echo "$response" | _egrep_o '.*?<\/total>' | sed -n 's/.*\(.*\)<\/total>.*/\1/p')" + + _debug total "$total" + + if [ "$results_retrieved" -eq "$total" ]; then + results_retrieved=0 + i="$(_math "$i" + 1)" + fi + done + return 1 +} + +_openprovider_request() { + request_xml=$1 + + xml_prefix='' + xml_content=$(printf '%s%s%s' "$OPENPROVIDER_USER" "$OPENPROVIDER_PASSWORDHASH" "$request_xml") + response="$(_post "$(echo "$xml_prefix$xml_content" | tr -d '\n')" "$OPENPROVIDER_API" "" "POST" "application/xml")" + _debug response "$response" + if ! _contains "$response" "0.*"; then + _err "API request failed." + return 1 + fi +} diff --git a/dnsapi/dns_openstack.sh b/dnsapi/dns_openstack.sh new file mode 100755 index 00000000..fcc1dc2e --- /dev/null +++ b/dnsapi/dns_openstack.sh @@ -0,0 +1,348 @@ +#!/usr/bin/env sh + +# OpenStack Designate API plugin +# +# This requires you to have OpenStackClient and python-desginateclient +# installed. +# +# You will require Keystone V3 credentials loaded into your environment, which +# could be either password or v3applicationcredential type. +# +# Author: Andy Botting + +######## Public functions ##################### + +# Usage: dns_openstack_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_openstack_add() { + fulldomain=$1 + txtvalue=$2 + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + _dns_openstack_credentials || return $? + _dns_openstack_check_setup || return $? + _dns_openstack_find_zone || return $? + _dns_openstack_get_recordset || return $? + _debug _recordset_id "$_recordset_id" + if [ -n "$_recordset_id" ]; then + _dns_openstack_get_records || return $? + _debug _records "$_records" + fi + _dns_openstack_create_recordset || return $? +} + +# Usage: dns_openstack_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Remove the txt record after validation. +dns_openstack_rm() { + fulldomain=$1 + txtvalue=$2 + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + _dns_openstack_credentials || return $? + _dns_openstack_check_setup || return $? + _dns_openstack_find_zone || return $? + _dns_openstack_get_recordset || return $? + _debug _recordset_id "$_recordset_id" + if [ -n "$_recordset_id" ]; then + _dns_openstack_get_records || return $? + _debug _records "$_records" + fi + _dns_openstack_delete_recordset || return $? +} + +#################### Private functions below ################################## + +_dns_openstack_create_recordset() { + + if [ -z "$_recordset_id" ]; then + _info "Creating a new recordset" + if ! _recordset_id=$(openstack recordset create -c id -f value --type TXT --record="$txtvalue" "$_zone_id" "$fulldomain."); then + _err "No recordset ID found after create" + return 1 + fi + else + _info "Updating existing recordset" + # Build new list of --record= args for update + _record_args="--record=$txtvalue" + for _rec in $_records; do + _record_args="$_record_args --record=$_rec" + done + # shellcheck disable=SC2086 + if ! _recordset_id=$(openstack recordset set -c id -f value $_record_args "$_zone_id" "$fulldomain."); then + _err "Recordset update failed" + return 1 + fi + fi + + _max_retries=60 + _sleep_sec=5 + _retry_times=0 + while [ "$_retry_times" -lt "$_max_retries" ]; do + _retry_times=$(_math "$_retry_times" + 1) + _debug3 _retry_times "$_retry_times" + + _record_status=$(openstack recordset show -c status -f value "$_zone_id" "$_recordset_id") + _info "Recordset status is $_record_status" + if [ "$_record_status" = "ACTIVE" ]; then + return 0 + elif [ "$_record_status" = "ERROR" ]; then + return 1 + else + _sleep $_sleep_sec + fi + done + + _err "Recordset failed to become ACTIVE" + return 1 +} + +_dns_openstack_delete_recordset() { + + if [ "$_records" = "$txtvalue" ]; then + _info "Only one record found, deleting recordset" + if ! openstack recordset delete "$_zone_id" "$fulldomain." >/dev/null; then + _err "Failed to delete recordset" + return 1 + fi + else + _info "Found existing records, updating recordset" + # Build new list of --record= args for update + _record_args="" + for _rec in $_records; do + if [ "$_rec" = "$txtvalue" ]; then + continue + fi + _record_args="$_record_args --record=$_rec" + done + # shellcheck disable=SC2086 + if ! openstack recordset set -c id -f value $_record_args "$_zone_id" "$fulldomain." >/dev/null; then + _err "Recordset update failed" + return 1 + fi + fi +} + +_dns_openstack_get_root() { + # Take the full fqdn and strip away pieces until we get an exact zone name + # match. For example, _acme-challenge.something.domain.com might need to go + # into something.domain.com or domain.com + _zone_name=$1 + _zone_list=$2 + while [ "$_zone_name" != "" ]; do + _zone_name="$(echo "$_zone_name" | sed 's/[^.]*\.*//')" + echo "$_zone_list" | while read -r id name; do + if _startswith "$_zone_name." "$name"; then + echo "$id" + fi + done + done | _head_n 1 +} + +_dns_openstack_find_zone() { + if ! _zone_list="$(openstack zone list -c id -c name -f value)"; then + _err "Can't list zones. Check your OpenStack credentials" + return 1 + fi + _debug _zone_list "$_zone_list" + + if ! _zone_id="$(_dns_openstack_get_root "$fulldomain" "$_zone_list")"; then + _err "Can't find a matching zone. Check your OpenStack credentials" + return 1 + fi + _debug _zone_id "$_zone_id" +} + +_dns_openstack_get_records() { + if ! _records=$(openstack recordset show -c records -f value "$_zone_id" "$fulldomain."); then + _err "Failed to get records" + return 1 + fi + return 0 +} + +_dns_openstack_get_recordset() { + if ! _recordset_id=$(openstack recordset list -c id -f value --name "$fulldomain." "$_zone_id"); then + _err "Failed to get recordset" + return 1 + fi + return 0 +} + +_dns_openstack_check_setup() { + if ! _exists openstack; then + _err "OpenStack client not found" + return 1 + fi +} + +_dns_openstack_credentials() { + _debug "Check OpenStack credentials" + + # If we have OS_AUTH_URL already set in the environment, then assume we want + # to use those, otherwise use stored credentials + if [ -n "$OS_AUTH_URL" ]; then + _debug "OS_AUTH_URL env var found, using environment" + else + _debug "OS_AUTH_URL not found, loading stored credentials" + OS_AUTH_URL="${OS_AUTH_URL:-$(_readaccountconf_mutable OS_AUTH_URL)}" + OS_IDENTITY_API_VERSION="${OS_IDENTITY_API_VERSION:-$(_readaccountconf_mutable OS_IDENTITY_API_VERSION)}" + OS_AUTH_TYPE="${OS_AUTH_TYPE:-$(_readaccountconf_mutable OS_AUTH_TYPE)}" + OS_APPLICATION_CREDENTIAL_ID="${OS_APPLICATION_CREDENTIAL_ID:-$(_readaccountconf_mutable OS_APPLICATION_CREDENTIAL_ID)}" + OS_APPLICATION_CREDENTIAL_SECRET="${OS_APPLICATION_CREDENTIAL_SECRET:-$(_readaccountconf_mutable OS_APPLICATION_CREDENTIAL_SECRET)}" + OS_USERNAME="${OS_USERNAME:-$(_readaccountconf_mutable OS_USERNAME)}" + OS_PASSWORD="${OS_PASSWORD:-$(_readaccountconf_mutable OS_PASSWORD)}" + OS_PROJECT_NAME="${OS_PROJECT_NAME:-$(_readaccountconf_mutable OS_PROJECT_NAME)}" + OS_PROJECT_ID="${OS_PROJECT_ID:-$(_readaccountconf_mutable OS_PROJECT_ID)}" + OS_USER_DOMAIN_NAME="${OS_USER_DOMAIN_NAME:-$(_readaccountconf_mutable OS_USER_DOMAIN_NAME)}" + OS_USER_DOMAIN_ID="${OS_USER_DOMAIN_ID:-$(_readaccountconf_mutable OS_USER_DOMAIN_ID)}" + OS_PROJECT_DOMAIN_NAME="${OS_PROJECT_DOMAIN_NAME:-$(_readaccountconf_mutable OS_PROJECT_DOMAIN_NAME)}" + OS_PROJECT_DOMAIN_ID="${OS_PROJECT_DOMAIN_ID:-$(_readaccountconf_mutable OS_PROJECT_DOMAIN_ID)}" + fi + + # Check each var and either save or clear it depending on whether its set. + # The helps us clear out old vars in the case where a user may want + # to switch between password and app creds + _debug "OS_AUTH_URL" "$OS_AUTH_URL" + if [ -n "$OS_AUTH_URL" ]; then + export OS_AUTH_URL + _saveaccountconf_mutable OS_AUTH_URL "$OS_AUTH_URL" + else + unset OS_AUTH_URL + _clearaccountconf SAVED_OS_AUTH_URL + fi + + _debug "OS_IDENTITY_API_VERSION" "$OS_IDENTITY_API_VERSION" + if [ -n "$OS_IDENTITY_API_VERSION" ]; then + export OS_IDENTITY_API_VERSION + _saveaccountconf_mutable OS_IDENTITY_API_VERSION "$OS_IDENTITY_API_VERSION" + else + unset OS_IDENTITY_API_VERSION + _clearaccountconf SAVED_OS_IDENTITY_API_VERSION + fi + + _debug "OS_AUTH_TYPE" "$OS_AUTH_TYPE" + if [ -n "$OS_AUTH_TYPE" ]; then + export OS_AUTH_TYPE + _saveaccountconf_mutable OS_AUTH_TYPE "$OS_AUTH_TYPE" + else + unset OS_AUTH_TYPE + _clearaccountconf SAVED_OS_AUTH_TYPE + fi + + _debug "OS_APPLICATION_CREDENTIAL_ID" "$OS_APPLICATION_CREDENTIAL_ID" + if [ -n "$OS_APPLICATION_CREDENTIAL_ID" ]; then + export OS_APPLICATION_CREDENTIAL_ID + _saveaccountconf_mutable OS_APPLICATION_CREDENTIAL_ID "$OS_APPLICATION_CREDENTIAL_ID" + else + unset OS_APPLICATION_CREDENTIAL_ID + _clearaccountconf SAVED_OS_APPLICATION_CREDENTIAL_ID + fi + + _secure_debug "OS_APPLICATION_CREDENTIAL_SECRET" "$OS_APPLICATION_CREDENTIAL_SECRET" + if [ -n "$OS_APPLICATION_CREDENTIAL_SECRET" ]; then + export OS_APPLICATION_CREDENTIAL_SECRET + _saveaccountconf_mutable OS_APPLICATION_CREDENTIAL_SECRET "$OS_APPLICATION_CREDENTIAL_SECRET" + else + unset OS_APPLICATION_CREDENTIAL_SECRET + _clearaccountconf SAVED_OS_APPLICATION_CREDENTIAL_SECRET + fi + + _debug "OS_USERNAME" "$OS_USERNAME" + if [ -n "$OS_USERNAME" ]; then + export OS_USERNAME + _saveaccountconf_mutable OS_USERNAME "$OS_USERNAME" + else + unset OS_USERNAME + _clearaccountconf SAVED_OS_USERNAME + fi + + _secure_debug "OS_PASSWORD" "$OS_PASSWORD" + if [ -n "$OS_PASSWORD" ]; then + export OS_PASSWORD + _saveaccountconf_mutable OS_PASSWORD "$OS_PASSWORD" + else + unset OS_PASSWORD + _clearaccountconf SAVED_OS_PASSWORD + fi + + _debug "OS_PROJECT_NAME" "$OS_PROJECT_NAME" + if [ -n "$OS_PROJECT_NAME" ]; then + export OS_PROJECT_NAME + _saveaccountconf_mutable OS_PROJECT_NAME "$OS_PROJECT_NAME" + else + unset OS_PROJECT_NAME + _clearaccountconf SAVED_OS_PROJECT_NAME + fi + + _debug "OS_PROJECT_ID" "$OS_PROJECT_ID" + if [ -n "$OS_PROJECT_ID" ]; then + export OS_PROJECT_ID + _saveaccountconf_mutable OS_PROJECT_ID "$OS_PROJECT_ID" + else + unset OS_PROJECT_ID + _clearaccountconf SAVED_OS_PROJECT_ID + fi + + _debug "OS_USER_DOMAIN_NAME" "$OS_USER_DOMAIN_NAME" + if [ -n "$OS_USER_DOMAIN_NAME" ]; then + export OS_USER_DOMAIN_NAME + _saveaccountconf_mutable OS_USER_DOMAIN_NAME "$OS_USER_DOMAIN_NAME" + else + unset OS_USER_DOMAIN_NAME + _clearaccountconf SAVED_OS_USER_DOMAIN_NAME + fi + + _debug "OS_USER_DOMAIN_ID" "$OS_USER_DOMAIN_ID" + if [ -n "$OS_USER_DOMAIN_ID" ]; then + export OS_USER_DOMAIN_ID + _saveaccountconf_mutable OS_USER_DOMAIN_ID "$OS_USER_DOMAIN_ID" + else + unset OS_USER_DOMAIN_ID + _clearaccountconf SAVED_OS_USER_DOMAIN_ID + fi + + _debug "OS_PROJECT_DOMAIN_NAME" "$OS_PROJECT_DOMAIN_NAME" + if [ -n "$OS_PROJECT_DOMAIN_NAME" ]; then + export OS_PROJECT_DOMAIN_NAME + _saveaccountconf_mutable OS_PROJECT_DOMAIN_NAME "$OS_PROJECT_DOMAIN_NAME" + else + unset OS_PROJECT_DOMAIN_NAME + _clearaccountconf SAVED_OS_PROJECT_DOMAIN_NAME + fi + + _debug "OS_PROJECT_DOMAIN_ID" "$OS_PROJECT_DOMAIN_ID" + if [ -n "$OS_PROJECT_DOMAIN_ID" ]; then + export OS_PROJECT_DOMAIN_ID + _saveaccountconf_mutable OS_PROJECT_DOMAIN_ID "$OS_PROJECT_DOMAIN_ID" + else + unset OS_PROJECT_DOMAIN_ID + _clearaccountconf SAVED_OS_PROJECT_DOMAIN_ID + fi + + if [ "$OS_AUTH_TYPE" = "v3applicationcredential" ]; then + # Application Credential auth + if [ -z "$OS_APPLICATION_CREDENTIAL_ID" ] || [ -z "$OS_APPLICATION_CREDENTIAL_SECRET" ]; then + _err "When using OpenStack application credentials, OS_APPLICATION_CREDENTIAL_ID" + _err "and OS_APPLICATION_CREDENTIAL_SECRET must be set." + _err "Please check your credentials and try again." + return 1 + fi + else + # Password auth + if [ -z "$OS_USERNAME" ] || [ -z "$OS_PASSWORD" ]; then + _err "OpenStack username or password not found." + _err "Please check your credentials and try again." + return 1 + fi + + if [ -z "$OS_PROJECT_NAME" ] && [ -z "$OS_PROJECT_ID" ]; then + _err "When using password authentication, OS_PROJECT_NAME or" + _err "OS_PROJECT_ID must be set." + _err "Please check your credentials and try again." + return 1 + fi + fi + + return 0 +} diff --git a/dnsapi/dns_opnsense.sh b/dnsapi/dns_opnsense.sh new file mode 100755 index 00000000..d40cbe28 --- /dev/null +++ b/dnsapi/dns_opnsense.sh @@ -0,0 +1,272 @@ +#!/usr/bin/env sh + +#OPNsense Bind API +#https://docs.opnsense.org/development/api.html +# +#OPNs_Host="opnsense.example.com" +#OPNs_Port="443" +# optional, defaults to 443 if unset +#OPNs_Key="qocfU9RSbt8vTIBcnW8bPqCrpfAHMDvj5OzadE7Str+rbjyCyk7u6yMrSCHtBXabgDDXx/dY0POUp7ZA" +#OPNs_Token="pZEQ+3ce8dDlfBBdg3N8EpqpF5I1MhFqdxX06le6Gl8YzyQvYCfCzNaFX9O9+IOSyAs7X71fwdRiZ+Lv" +#OPNs_Api_Insecure=0 +# optional, defaults to 0 if unset +# Set 1 for insecure and 0 for secure -> difference is whether ssl cert is checked for validity (0) or whether it is just accepted (1) + +######## Public functions ##################### +#Usage: add _acme-challenge.www.domain.com "123456789ABCDEF0000000000000000000000000000000000000" +#fulldomain +#txtvalue +OPNs_DefaultPort=443 +OPNs_DefaultApi_Insecure=0 + +dns_opnsense_add() { + fulldomain=$1 + txtvalue=$2 + + _opns_check_auth || return 1 + + if ! set_record "$fulldomain" "$txtvalue"; then + return 1 + fi + + return 0 +} + +#fulldomain +dns_opnsense_rm() { + fulldomain=$1 + txtvalue=$2 + + _opns_check_auth || return 1 + + if ! rm_record "$fulldomain" "$txtvalue"; then + return 1 + fi + + return 0 +} + +set_record() { + fulldomain=$1 + new_challenge=$2 + _info "Adding record $fulldomain with challenge: $new_challenge" + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _domain "$_domain" + _debug _host "$_host" + _debug _domainid "$_domainid" + _return_str="" + _record_string="" + _build_record_string "$_domainid" "$_host" "$new_challenge" + _uuid="" + if _existingchallenge "$_domain" "$_host" "$new_challenge"; then + # Update + if _opns_rest "POST" "/record/setRecord/${_uuid}" "$_record_string"; then + _return_str="$response" + else + return 1 + fi + + else + #create + if _opns_rest "POST" "/record/addRecord" "$_record_string"; then + _return_str="$response" + else + return 1 + fi + fi + + if echo "$_return_str" | _egrep_o "\"result\":\"saved\"" >/dev/null; then + _opns_rest "POST" "/service/reconfigure" "{}" + _debug "Record created" + else + _err "Error creating record $_record_string" + return 1 + fi + + return 0 +} + +rm_record() { + fulldomain=$1 + new_challenge="$2" + _info "Remove record $fulldomain with challenge: $new_challenge" + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _domain "$_domain" + _debug _host "$_host" + _debug _domainid "$_domainid" + _uuid="" + if _existingchallenge "$_domain" "$_host" "$new_challenge"; then + # Delete + if _opns_rest "POST" "/record/delRecord/${_uuid}" "\{\}"; then + if echo "$_return_str" | _egrep_o "\"result\":\"deleted\"" >/dev/null; then + _opns_rest "POST" "/service/reconfigure" "{}" + _debug "Record deleted" + else + _err "Error deleting record $_host from domain $fulldomain" + return 1 + fi + else + _err "Error deleting record $_host from domain $fulldomain" + return 1 + fi + else + _info "Record not found, nothing to remove" + fi + + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _domainid=domid +#_domain=domain.com +_get_root() { + domain=$1 + i=2 + p=1 + if _opns_rest "GET" "/domain/searchPrimaryDomain"; then + _domain_response="$response" + else + return 1 + fi + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + _debug h "$h" + id=$(echo "$_domain_response" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"1\",\"type\":\"primary\",\"domainname\":\"${h}\"" | cut -d ':' -f 2 | cut -d '"' -f 2) + if [ -n "$id" ]; then + _debug id "$id" + _host=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="${h}" + _domainid="${id}" + return 0 + fi + p=$i + i=$(_math $i + 1) + done + _debug "$domain not found" + + return 1 +} + +_opns_rest() { + method=$1 + ep=$2 + data=$3 + #Percent encode user and token + key=$(echo "$OPNs_Key" | tr -d "\n\r" | _url_encode) + token=$(echo "$OPNs_Token" | tr -d "\n\r" | _url_encode) + + opnsense_url="https://${key}:${token}@${OPNs_Host}:${OPNs_Port:-$OPNs_DefaultPort}/api/bind${ep}" + export _H1="Content-Type: application/json" + _debug2 "Try to call api: https://${OPNs_Host}:${OPNs_Port:-$OPNs_DefaultPort}/api/bind${ep}" + if [ ! "$method" = "GET" ]; then + _debug data "$data" + export _H1="Content-Type: application/json" + response="$(_post "$data" "$opnsense_url" "" "$method")" + else + export _H1="" + response="$(_get "$opnsense_url")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + + return 0 +} + +_build_record_string() { + _record_string="{\"record\":{\"enabled\":\"1\",\"domain\":\"$1\",\"name\":\"$2\",\"type\":\"TXT\",\"value\":\"$3\"}}" +} + +_existingchallenge() { + if _opns_rest "GET" "/record/searchRecord"; then + _record_response="$response" + else + return 1 + fi + _uuid="" + _uuid=$(echo "$_record_response" | _egrep_o "\"uuid\":\"[^\"]*\",\"enabled\":\"[01]\",\"domain\":\"$1\",\"name\":\"$2\",\"type\":\"TXT\",\"value\":\"$3\"" | cut -d ':' -f 2 | cut -d '"' -f 2) + + if [ -n "$_uuid" ]; then + _debug uuid "$_uuid" + return 0 + fi + _debug "${2}.$1{1} record not found" + + return 1 +} + +_opns_check_auth() { + OPNs_Host="${OPNs_Host:-$(_readaccountconf_mutable OPNs_Host)}" + OPNs_Port="${OPNs_Port:-$(_readaccountconf_mutable OPNs_Port)}" + OPNs_Key="${OPNs_Key:-$(_readaccountconf_mutable OPNs_Key)}" + OPNs_Token="${OPNs_Token:-$(_readaccountconf_mutable OPNs_Token)}" + OPNs_Api_Insecure="${OPNs_Api_Insecure:-$(_readaccountconf_mutable OPNs_Api_Insecure)}" + + if [ -z "$OPNs_Host" ]; then + _err "You don't specify OPNsense address." + return 1 + else + _saveaccountconf_mutable OPNs_Host "$OPNs_Host" + fi + + if ! printf '%s' "$OPNs_Port" | grep '^[0-9]*$' >/dev/null; then + _err 'OPNs_Port specified but not numeric value' + return 1 + elif [ -z "$OPNs_Port" ]; then + _info "OPNSense port not specified. Defaulting to using port $OPNs_DefaultPort" + else + _saveaccountconf_mutable OPNs_Port "$OPNs_Port" + fi + + if ! printf '%s' "$OPNs_Api_Insecure" | grep '^[01]$' >/dev/null; then + _err 'OPNs_Api_Insecure specified but not 0/1 value' + return 1 + elif [ -n "$OPNs_Api_Insecure" ]; then + _saveaccountconf_mutable OPNs_Api_Insecure "$OPNs_Api_Insecure" + fi + export HTTPS_INSECURE="${OPNs_Api_Insecure:-$OPNs_DefaultApi_Insecure}" + + if [ -z "$OPNs_Key" ]; then + _err "you have not specified your OPNsense api key id." + _err "Please set OPNs_Key and try again." + return 1 + else + _saveaccountconf_mutable OPNs_Key "$OPNs_Key" + fi + + if [ -z "$OPNs_Token" ]; then + _err "you have not specified your OPNsense token." + _err "Please create OPNs_Token and try again." + return 1 + else + _saveaccountconf_mutable OPNs_Token "$OPNs_Token" + fi + + if ! _opns_rest "GET" "/general/get"; then + _err "Call to OPNsense API interface failed. Unable to access OPNsense API." + return 1 + fi + return 0 +} diff --git a/dnsapi/dns_ovh.sh b/dnsapi/dns_ovh.sh index 2669cc86..e1a958f6 100755 --- a/dnsapi/dns_ovh.sh +++ b/dnsapi/dns_ovh.sh @@ -14,6 +14,9 @@ #'ovh-eu' OVH_EU='https://eu.api.ovh.com/1.0' +#'ovh-us' +OVH_US='https://api.us.ovhcloud.com/1.0' + #'ovh-ca': OVH_CA='https://ca.api.ovh.com/1.0' @@ -29,52 +32,54 @@ SYS_EU='https://eu.api.soyoustart.com/1.0' #'soyoustart-ca' SYS_CA='https://ca.api.soyoustart.com/1.0' -#'runabove-ca' -RAV_CA='https://api.runabove.com/1.0' +wiki="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-OVH-domain-api" -wiki="https://github.com/Neilpang/acme.sh/wiki/How-to-use-OVH-domain-api" - -ovh_success="https://github.com/Neilpang/acme.sh/wiki/OVH-Success" +ovh_success="https://github.com/acmesh-official/acme.sh/wiki/OVH-Success" _ovh_get_api() { _ogaep="$1" case "${_ogaep}" in - ovh-eu | ovheu) - printf "%s" $OVH_EU - return - ;; - ovh-ca | ovhca) - printf "%s" $OVH_CA - return - ;; - kimsufi-eu | kimsufieu) - printf "%s" $KSF_EU - return - ;; - kimsufi-ca | kimsufica) - printf "%s" $KSF_CA - return - ;; - soyoustart-eu | soyoustarteu) - printf "%s" $SYS_EU - return - ;; - soyoustart-ca | soyoustartca) - printf "%s" $SYS_CA - return - ;; - runabove-ca | runaboveca) - printf "%s" $RAV_CA - return - ;; + ovh-eu | ovheu) + printf "%s" $OVH_EU + return + ;; + ovh-us | ovhus) + printf "%s" $OVH_US + return + ;; + ovh-ca | ovhca) + printf "%s" $OVH_CA + return + ;; + kimsufi-eu | kimsufieu) + printf "%s" $KSF_EU + return + ;; + kimsufi-ca | kimsufica) + printf "%s" $KSF_CA + return + ;; + soyoustart-eu | soyoustarteu) + printf "%s" $SYS_EU + return + ;; + soyoustart-ca | soyoustartca) + printf "%s" $SYS_CA + return + ;; + # raw API url starts with https:// + https*) + printf "%s" "$1" + return + ;; - *) + *) - _err "Unknown parameter : $1" - return 1 - ;; + _err "Unknown endpoint : $1" + return 1 + ;; esac } @@ -92,7 +97,7 @@ _initAuth() { if [ "$OVH_AK" != "$(_readaccountconf OVH_AK)" ]; then _info "It seems that your ovh key is changed, let's clear consumer key first." - _clearaccountconf OVH_CK + _clearaccountconf_mutable OVH_CK fi _saveaccountconf_mutable OVH_AK "$OVH_AK" _saveaccountconf_mutable OVH_AS "$OVH_AS" @@ -118,13 +123,14 @@ _initAuth() { #return and wait for retry. return 1 fi + _saveaccountconf_mutable OVH_CK "$OVH_CK" _info "Checking authentication" - if ! _ovh_rest GET "domain" || _contains "$response" "INVALID_CREDENTIAL"; then + if ! _ovh_rest GET "domain" || _contains "$response" "INVALID_CREDENTIAL" || _contains "$response" "NOT_CREDENTIAL"; then _err "The consumer key is invalid: $OVH_CK" _err "Please retry to create a new one." - _clearaccountconf OVH_CK + _clearaccountconf_mutable OVH_CK return 1 fi _info "Consumer key is ok." @@ -198,6 +204,8 @@ dns_ovh_rm() { if ! _ovh_rest DELETE "domain/zone/$_domain/record/$rid"; then return 1 fi + _ovh_rest POST "domain/zone/$_domain/refresh" + _debug "Refresh:$response" return 0 fi done @@ -233,8 +241,7 @@ _ovh_authentication() { _secure_debug consumerKey "$consumerKey" OVH_CK="$consumerKey" - _saveaccountconf OVH_CK "$OVH_CK" - + _saveaccountconf_mutable OVH_CK "$OVH_CK" _info "Please open this link to do authentication: $(__green "$validationUrl")" _info "Here is a guide for you: $(__green "$wiki")" @@ -248,7 +255,7 @@ _ovh_authentication() { # _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) @@ -261,7 +268,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_pleskxml.sh b/dnsapi/dns_pleskxml.sh new file mode 100644 index 00000000..81973e07 --- /dev/null +++ b/dnsapi/dns_pleskxml.sh @@ -0,0 +1,455 @@ +#!/usr/bin/env sh + +## Name: dns_pleskxml.sh +## Created by Stilez. +## Also uses some code from PR#1832 by @romanlum (https://github.com/acmesh-official/acme.sh/pull/1832/files) + +## This DNS-01 method uses the Plesk XML API described at: +## https://docs.plesk.com/en-US/12.5/api-rpc/about-xml-api.28709 +## and more specifically: https://docs.plesk.com/en-US/12.5/api-rpc/reference.28784 + +## Note: a DNS ID with host = empty string is OK for this API, see +## https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-dns/managing-dns-records/adding-dns-record.34798 +## For example, to add a TXT record to DNS alias domain "acme-alias.com" would be a valid Plesk action. +## So this API module can handle such a request, if needed. + +## For ACME v2 purposes, new TXT records are appended when added, and removing one TXT record will not affect any other TXT records. + +## The user credentials (username+password) and URL/URI for the Plesk XML API must be set by the user +## before this module is called (case sensitive): +## +## ``` +## export pleskxml_uri="https://address-of-my-plesk-server.net:8443/enterprise/control/agent.php" +## (or probably something similar) +## export pleskxml_user="my plesk username" +## export pleskxml_pass="my plesk password" +## ``` + +## Ok, let's issue a cert now: +## ``` +## acme.sh --issue --dns dns_pleskxml -d example.com -d www.example.com +## ``` +## +## The `pleskxml_uri`, `pleskxml_user` and `pleskxml_pass` will be saved in `~/.acme.sh/account.conf` and reused when needed. + +#################### INTERNAL VARIABLES + NEWLINE + API TEMPLATES ################################## + +pleskxml_init_checks_done=0 + +# Variable containing bare newline - not a style issue +# shellcheck disable=SC1004 +NEWLINE='\ +' + +pleskxml_tplt_get_domains="" +# Get a list of domains that PLESK can manage, so we can check root domain + host for acme.sh +# Also used to test credentials and URI. +# No params. + +pleskxml_tplt_get_additional_domains="" +# Get a list of additional domains that PLESK can manage, so we can check root domain + host for acme.sh +# No params. + +pleskxml_tplt_get_dns_records="%s" +# Get all DNS records for a Plesk domain ID. +# PARAM = Plesk domain id to query + +pleskxml_tplt_add_txt_record="%sTXT%s%s" +# Add a TXT record to a domain. +# PARAMS = (1) Plesk internal domain ID, (2) "hostname" for the new record, eg '_acme_challenge', (3) TXT record value + +pleskxml_tplt_rmv_dns_record="%s" +# Delete a specific TXT record from a domain. +# PARAM = the Plesk internal ID for the DNS record to be deleted + +#################### Public functions ################################## + +#Usage: dns_pleskxml_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_pleskxml_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Entering dns_pleskxml_add() to add TXT record '$txtvalue' to domain '$fulldomain'..." + + # Get credentials if not already checked, and confirm we can log in to Plesk XML API + if ! _credential_check; then + return 1 + fi + + # Get root and subdomain details, and Plesk domain ID + if ! _pleskxml_get_root_domain "$fulldomain"; then + return 1 + fi + + _debug 'Credentials OK, and domain identified. Calling Plesk XML API to add TXT record' + + # printf using template in a variable - not a style issue + # shellcheck disable=SC2059 + request="$(printf "$pleskxml_tplt_add_txt_record" "$root_domain_id" "$sub_domain_name" "$txtvalue")" + if ! _call_api "$request"; then + return 1 + fi + + # OK, we should have added a TXT record. Let's check and return success if so. + # All that should be left in the result, is one section, containing okNEW_DNS_RECORD_ID + + results="$(_api_response_split "$pleskxml_prettyprint_result" 'result' '')" + + if ! _value "$results" | grep 'ok' | grep '[0-9]\{1,\}' >/dev/null; then + # Error - doesn't contain expected string. Something's wrong. + _err 'Error when calling Plesk XML API.' + _err 'The result did not contain the expected XXXXX section, or contained other values as well.' + _err 'This is unexpected: something has gone wrong.' + _err 'The full response was:' + _err "$pleskxml_prettyprint_result" + return 1 + fi + + recid="$(_value "$results" | grep '[0-9]\{1,\}' | sed 's/^.*\([0-9]\{1,\}\)<\/id>.*$/\1/')" + + _info "Success. TXT record appears to be correctly added (Plesk record ID=$recid). Exiting dns_pleskxml_add()." + + return 0 +} + +#Usage: dns_pleskxml_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_pleskxml_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Entering dns_pleskxml_rm() to remove TXT record '$txtvalue' from domain '$fulldomain'..." + + # Get credentials if not already checked, and confirm we can log in to Plesk XML API + if ! _credential_check; then + return 1 + fi + + # Get root and subdomain details, and Plesk domain ID + if ! _pleskxml_get_root_domain "$fulldomain"; then + return 1 + fi + + _debug 'Credentials OK, and domain identified. Calling Plesk XML API to get list of TXT records and their IDs' + + # printf using template in a variable - not a style issue + # shellcheck disable=SC2059 + request="$(printf "$pleskxml_tplt_get_dns_records" "$root_domain_id")" + if ! _call_api "$request"; then + return 1 + fi + + # Reduce output to one line per DNS record, filtered for TXT records with a record ID only (which they should all have) + # Also strip out spaces between tags, redundant and group tags and any tags + reclist="$( + _api_response_split "$pleskxml_prettyprint_result" 'result' 'ok' | + sed 's# \{1,\}<\([a-zA-Z]\)#<\1#g;s###g;s#<[a-z][^/<>]*/>##g' | + grep "${root_domain_id}" | + grep '[0-9]\{1,\}' | + grep 'TXT' + )" + + if [ -z "$reclist" ]; then + _err "No TXT records found for root domain $fulldomain (Plesk domain ID ${root_domain_id}). Exiting." + return 1 + fi + + _debug "Got list of DNS TXT records for root Plesk domain ID ${root_domain_id} of root domain $fulldomain:" + _debug "$reclist" + + # Extracting the id of the TXT record for the full domain (NOT case-sensitive) and corresponding value + recid="$( + _value "$reclist" | + grep -i "${fulldomain}." | + grep "${txtvalue}" | + sed 's/^.*\([0-9]\{1,\}\)<\/id>.*$/\1/' + )" + + _debug "Got id from line: $recid" + + if ! _value "$recid" | grep '^[0-9]\{1,\}$' >/dev/null; then + _err "DNS records for root domain '${fulldomain}.' (Plesk ID ${root_domain_id}) + host '${sub_domain_name}' do not contain the TXT record '${txtvalue}'" + _err "Cannot delete TXT record. Exiting." + return 1 + fi + + _debug "Found Plesk record ID for target text string '${txtvalue}': ID=${recid}" + _debug 'Calling Plesk XML API to remove TXT record' + + # printf using template in a variable - not a style issue + # shellcheck disable=SC2059 + request="$(printf "$pleskxml_tplt_rmv_dns_record" "$recid")" + if ! _call_api "$request"; then + return 1 + fi + + # OK, we should have removed a TXT record. Let's check and return success if so. + # All that should be left in the result, is one section, containing okPLESK_DELETED_DNS_RECORD_ID + + results="$(_api_response_split "$pleskxml_prettyprint_result" 'result' '')" + + if ! _value "$results" | grep 'ok' | grep '[0-9]\{1,\}' >/dev/null; then + # Error - doesn't contain expected string. Something's wrong. + _err 'Error when calling Plesk XML API.' + _err 'The result did not contain the expected XXXXX section, or contained other values as well.' + _err 'This is unexpected: something has gone wrong.' + _err 'The full response was:' + _err "$pleskxml_prettyprint_result" + return 1 + fi + + _info "Success. TXT record appears to be correctly removed. Exiting dns_pleskxml_rm()." + return 0 +} + +#################### Private functions below (utility functions) ################################## + +# Outputs value of a variable without additional newlines etc +_value() { + printf '%s' "$1" +} + +# Outputs value of a variable (FQDN) and cuts it at 2 specified '.' delimiters, returning the text in between +# $1, $2 = where to cut +# $3 = FQDN +_valuecut() { + printf '%s' "$3" | cut -d . -f "${1}-${2}" +} + +# Counts '.' present in a domain name or other string +# $1 = domain name +_countdots() { + _value "$1" | tr -dc '.' | wc -c | sed 's/ //g' +} + +# Cleans up an API response, splits it "one line per item in the response" and greps for a string that in the context, identifies "useful" lines +# $1 - result string from API +# $2 - plain text tag to resplit on (usually "result" or "domain"). NOT REGEX +# $3 - basic regex to recognise useful return lines +# note: $3 matches via basic NOT extended regex (BRE), as extended regex capabilities not needed at the moment. +# Last line could change to instead, with suitable escaping of ['"/$], +# if future Plesk XML API changes ever require extended regex +_api_response_split() { + printf '%s' "$1" | + sed 's/^ +//;s/ +$//' | + tr -d '\n\r' | + sed "s/<\/\{0,1\}$2>/${NEWLINE}/g" | + grep "$3" +} + +#################### Private functions below (DNS functions) ################################## + +# Calls Plesk XML API, and checks results for obvious issues +_call_api() { + request="$1" + errtext='' + + _debug 'Entered _call_api(). Calling Plesk XML API with request:' + _debug "'$request'" + + export _H1="HTTP_AUTH_LOGIN: $pleskxml_user" + export _H2="HTTP_AUTH_PASSWD: $pleskxml_pass" + export _H3="content-Type: text/xml" + export _H4="HTTP_PRETTY_PRINT: true" + pleskxml_prettyprint_result="$(_post "${request}" "$pleskxml_uri" "" "POST")" + pleskxml_retcode="$?" + _debug 'The responses from the Plesk XML server were:' + _debug "retcode=$pleskxml_retcode. Literal response:" + _debug "'$pleskxml_prettyprint_result'" + + # Detect any that isn't "ok". None of the used calls should fail if the API is working correctly. + # Also detect if there simply aren't any status lines (null result?) and report that, as well. + # Remove structure from result string, since it might contain values that are related to the status of the domain and not to the API request + + statuslines_count_total="$(echo "$pleskxml_prettyprint_result" | sed '//,/<\/data>/d' | grep -c '^ *[^<]* *$')" + statuslines_count_okay="$(echo "$pleskxml_prettyprint_result" | sed '//,/<\/data>/d' | grep -c '^ *ok *$')" + _debug "statuslines_count_total=$statuslines_count_total." + _debug "statuslines_count_okay=$statuslines_count_okay." + + if [ -z "$statuslines_count_total" ]; then + + # We have no status lines at all. Results are empty + errtext='The Plesk XML API unexpectedly returned an empty set of results for this call.' + + elif [ "$statuslines_count_okay" -ne "$statuslines_count_total" ]; then + + # We have some status lines that aren't "ok". Any available details are in API response fields "status" "errcode" and "errtext" + # Workaround for basic regex: + # - filter output to keep only lines like this: "SPACEStextSPACES" (shouldn't be necessary with prettyprint but guarantees subsequent code is ok) + # - then edit the 3 "useful" error tokens individually and remove closing tags on all lines + # - then filter again to remove all lines not edited (which will be the lines not starting A-Z) + errtext="$( + _value "$pleskxml_prettyprint_result" | + grep '^ *<[a-z]\{1,\}>[^<]*<\/[a-z]\{1,\}> *$' | + sed 's/^ */Status: /;s/^ */Error code: /;s/^ */Error text: /;s/<\/.*$//' | + grep '^[A-Z]' + )" + + fi + + if [ "$pleskxml_retcode" -ne 0 ] || [ "$errtext" != "" ]; then + # Call failed, for reasons either in the retcode or the response text... + + if [ "$pleskxml_retcode" -eq 0 ]; then + _err "The POST request was successfully sent to the Plesk server." + else + _err "The return code for the POST request was $pleskxml_retcode (non-zero = failure in submitting request to server)." + fi + + if [ "$errtext" != "" ]; then + _err 'The error responses received from the Plesk server were:' + _err "$errtext" + else + _err "No additional error messages were received back from the Plesk server" + fi + + _err "The Plesk XML API call failed." + return 1 + + fi + + _debug "Leaving _call_api(). Successful call." + + return 0 +} + +# Startup checks (credentials, URI) +_credential_check() { + _debug "Checking Plesk XML API login credentials and URI..." + + if [ "$pleskxml_init_checks_done" -eq 1 ]; then + _debug "Initial checks already done, no need to repeat. Skipped." + return 0 + fi + + pleskxml_user="${pleskxml_user:-$(_readaccountconf_mutable pleskxml_user)}" + pleskxml_pass="${pleskxml_pass:-$(_readaccountconf_mutable pleskxml_pass)}" + pleskxml_uri="${pleskxml_uri:-$(_readaccountconf_mutable pleskxml_uri)}" + + if [ -z "$pleskxml_user" ] || [ -z "$pleskxml_pass" ] || [ -z "$pleskxml_uri" ]; then + pleskxml_user="" + pleskxml_pass="" + pleskxml_uri="" + _err "You didn't specify one or more of the Plesk XML API username, password, or URI." + _err "Please create these and try again." + _err "Instructions are in the 'dns_pleskxml' plugin source code or in the acme.sh documentation." + return 1 + fi + + # Test the API is usable, by trying to read the list of managed domains... + _call_api "$pleskxml_tplt_get_domains" + if [ "$pleskxml_retcode" -ne 0 ]; then + _err 'Failed to access Plesk XML API.' + _err "Please check your login credentials and Plesk URI, and that the URI is reachable, and try again." + return 1 + fi + + _saveaccountconf_mutable pleskxml_uri "$pleskxml_uri" + _saveaccountconf_mutable pleskxml_user "$pleskxml_user" + _saveaccountconf_mutable pleskxml_pass "$pleskxml_pass" + + _debug "Test login to Plesk XML API successful. Login credentials and URI successfully saved to the acme.sh configuration file for future use." + + pleskxml_init_checks_done=1 + + return 0 +} + +# For a FQDN, identify the root domain managed by Plesk, its domain ID in Plesk, and the host if any. + +# IMPORTANT NOTE: a result with host = empty string is OK for this API, see +# https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-dns/managing-dns-records/adding-dns-record.34798 +# See notes at top of this file + +_pleskxml_get_root_domain() { + original_full_domain_name="$1" + + _debug "Identifying DNS root domain for '$original_full_domain_name' that is managed by the Plesk account." + + # test if the domain as provided is valid for splitting. + + if [ "$(_countdots "$original_full_domain_name")" -eq 0 ]; then + _err "Invalid domain. The ACME domain must contain at least two parts (aa.bb) to identify a domain and tld for the TXT record." + return 1 + fi + + _debug "Querying Plesk server for list of managed domains..." + + _call_api "$pleskxml_tplt_get_domains" + if [ "$pleskxml_retcode" -ne 0 ]; then + return 1 + fi + + # Generate a crude list of domains known to this Plesk account based on subscriptions. + # We convert tags to so it'll flag on a hit with either or fields, + # for non-Western character sets. + # Output will be one line per known domain, containing 2 tages and a single tag + # We don't actually need to check for type, name, *and* id, but it guarantees only usable lines are returned. + + output="$(_api_response_split "$pleskxml_prettyprint_result" 'result' 'ok' | sed 's///g;s/<\/ascii-name>/<\/name>/g' | grep '' | grep '')" + debug_output="$(printf "%s" "$output" | sed -n 's:.*\(.*\).*:\1:p')" + + _debug 'Domains managed by Plesk server are:' + _debug "$debug_output" + + _debug "Querying Plesk server for list of additional managed domains..." + + _call_api "$pleskxml_tplt_get_additional_domains" + if [ "$pleskxml_retcode" -ne 0 ]; then + return 1 + fi + + # Generate a crude list of additional domains known to this Plesk account based on sites. + # We convert tags to so it'll flag on a hit with either or fields, + # for non-Western character sets. + # Output will be one line per known domain, containing 2 tages and a single tag + # We don't actually need to check for type, name, *and* id, but it guarantees only usable lines are returned. + + output_additional="$(_api_response_split "$pleskxml_prettyprint_result" 'result' 'ok' | sed 's///g;s/<\/ascii-name>/<\/name>/g' | grep '' | grep '')" + debug_additional="$(printf "%s" "$output_additional" | sed -n 's:.*\(.*\).*:\1:p')" + + _debug 'Additional domains managed by Plesk server are:' + _debug "$debug_additional" + + # Concate the two outputs together. + + output="$(printf "%s" "$output $NEWLINE $output_additional")" + debug_output="$(printf "%s" "$output" | sed -n 's:.*\(.*\).*:\1:p')" + + _debug 'Domains (including additional) managed by Plesk server are:' + _debug "$debug_output" + + # loop and test if domain, or any parent domain, is managed by Plesk + # Loop until we don't have any '.' in the string we're testing as a candidate Plesk-managed domain + + root_domain_name="$original_full_domain_name" + + while true; do + + _debug "Checking if '$root_domain_name' is managed by the Plesk server..." + + root_domain_id="$(_value "$output" | grep "$root_domain_name" | _head_n 1 | sed 's/^.*\([0-9]\{1,\}\)<\/id>.*$/\1/')" + + if [ -n "$root_domain_id" ]; then + # Found a match + # SEE IMPORTANT NOTE ABOVE - THIS FUNCTION CAN RETURN HOST='', AND THAT'S OK FOR PLESK XML API WHICH ALLOWS IT. + # SO WE HANDLE IT AND DON'T PREVENT IT + sub_domain_name="$(_value "$original_full_domain_name" | sed "s/\.\{0,1\}${root_domain_name}"'$//')" + _info "Success. Matched host '$original_full_domain_name' to: DOMAIN '${root_domain_name}' (Plesk ID '${root_domain_id}'), HOST '${sub_domain_name}'. Returning." + return 0 + fi + + # No match, try next parent up (if any)... + + root_domain_name="$(_valuecut 2 1000 "$root_domain_name")" + + if [ "$(_countdots "$root_domain_name")" -eq 0 ]; then + _debug "No match, and next parent would be a TLD..." + _err "Cannot find '$original_full_domain_name' or any parent domain of it, in Plesk." + _err "Are you sure that this domain is managed by this Plesk server?" + return 1 + fi + + _debug "No match, trying next parent up..." + + done +} diff --git a/dnsapi/dns_pointhq.sh b/dnsapi/dns_pointhq.sh new file mode 100644 index 00000000..62313109 --- /dev/null +++ b/dnsapi/dns_pointhq.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env sh + +# +#PointHQ_Key="sdfsdfsdfljlbjkljlkjsdfoiwje" +# +#PointHQ_Email="xxxx@sss.com" + +PointHQ_Api="https://api.pointhq.com" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_pointhq_add() { + fulldomain=$1 + txtvalue=$2 + + PointHQ_Key="${PointHQ_Key:-$(_readaccountconf_mutable PointHQ_Key)}" + PointHQ_Email="${PointHQ_Email:-$(_readaccountconf_mutable PointHQ_Email)}" + if [ -z "$PointHQ_Key" ] || [ -z "$PointHQ_Email" ]; then + PointHQ_Key="" + PointHQ_Email="" + _err "You didn't specify a PointHQ API key and email yet." + _err "Please create the key and try again." + return 1 + fi + + if ! _contains "$PointHQ_Email" "@"; then + _err "It seems that the PointHQ_Email=$PointHQ_Email is not a valid email address." + _err "Please check and retry." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable PointHQ_Key "$PointHQ_Key" + _saveaccountconf_mutable PointHQ_Email "$PointHQ_Email" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + if _pointhq_rest POST "zones/$_domain/records" "{\"zone_record\": {\"name\":\"$_sub_domain\",\"record_type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":3600}}"; then + if printf -- "%s" "$response" | grep "$fulldomain" >/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 +} + +#fulldomain txtvalue +dns_pointhq_rm() { + fulldomain=$1 + txtvalue=$2 + + PointHQ_Key="${PointHQ_Key:-$(_readaccountconf_mutable PointHQ_Key)}" + PointHQ_Email="${PointHQ_Email:-$(_readaccountconf_mutable PointHQ_Email)}" + if [ -z "$PointHQ_Key" ] || [ -z "$PointHQ_Email" ]; then + PointHQ_Key="" + PointHQ_Email="" + _err "You didn't specify a PointHQ API key and email yet." + _err "Please create the key and try again." + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting txt records" + _pointhq_rest GET "zones/${_domain}/records?record_type=TXT&name=$_sub_domain" + + if ! printf "%s" "$response" | grep "^\[" >/dev/null; then + _err "Error" + return 1 + fi + + if [ "$response" = "[]" ]; then + _info "No records 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 ! _pointhq_rest DELETE "zones/$_domain/records/$record_id"; then + _err "Delete record error." + return 1 + fi + _contains "$response" '"status":"OK"' + fi +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=2 + 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 ! _pointhq_rest GET "zones"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_pointhq_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + _pointhq_auth=$(printf "%s:%s" "$PointHQ_Email" "$PointHQ_Key" | _base64) + + export _H1="Authorization: Basic $_pointhq_auth" + export _H2="Content-Type: application/json" + export _H3="Accept: application/json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$PointHQ_Api/$ep" "" "$m")" + else + response="$(_get "$PointHQ_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} 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_rackcorp.sh b/dnsapi/dns_rackcorp.sh new file mode 100644 index 00000000..6aabfddc --- /dev/null +++ b/dnsapi/dns_rackcorp.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env sh + +# Provider: RackCorp (www.rackcorp.com) +# Author: Stephen Dendtler (sdendtler@rackcorp.com) +# Report Bugs here: https://github.com/senjoo/acme.sh +# Alternate email contact: support@rackcorp.com +# +# You'll need an API key (Portal: ADMINISTRATION -> API) +# Set the environment variables as below: +# +# export RACKCORP_APIUUID="UUIDHERE" +# export RACKCORP_APISECRET="SECRETHERE" +# + +RACKCORP_API_ENDPOINT="https://api.rackcorp.net/api/rest/v2.4/json.php" + +######## Public functions ##################### + +dns_rackcorp_add() { + fulldomain="$1" + txtvalue="$2" + + _debug fulldomain="$fulldomain" + _debug txtvalue="$txtvalue" + + if ! _rackcorp_validate; then + return 1 + fi + + _debug "Searching for root zone" + if ! _get_root "$fulldomain"; then + return 1 + fi + _debug _lookup "$_lookup" + _debug _domain "$_domain" + + _info "Creating TXT record." + + if ! _rackcorp_api dns.record.create "\"name\":\"$_domain\",\"type\":\"TXT\",\"lookup\":\"$_lookup\",\"data\":\"$txtvalue\",\"ttl\":300"; then + return 1 + fi + + return 0 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_rackcorp_rm() { + fulldomain=$1 + txtvalue=$2 + + _debug fulldomain="$fulldomain" + _debug txtvalue="$txtvalue" + + if ! _rackcorp_validate; then + return 1 + fi + + _debug "Searching for root zone" + if ! _get_root "$fulldomain"; then + return 1 + fi + _debug _lookup "$_lookup" + _debug _domain "$_domain" + + _info "Creating TXT record." + + if ! _rackcorp_api dns.record.delete "\"name\":\"$_domain\",\"type\":\"TXT\",\"lookup\":\"$_lookup\",\"data\":\"$txtvalue\""; then + return 1 + fi + + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.domain.com +#returns +# _lookup=_acme-challenge +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + p=1 + if ! _rackcorp_api dns.domain.getall "\"name\":\"$domain\""; then + return 1 + fi + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug searchhost "$h" + if [ -z "$h" ]; then + _err "Could not find domain for record $domain in RackCorp using the provided credentials" + #not valid + return 1 + fi + + _rackcorp_api dns.domain.getall "\"exactName\":\"$h\"" + + if _contains "$response" "\"matches\":1"; then + if _contains "$response" "\"name\":\"$h\""; then + _lookup=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + fi + p=$i + i=$(_math "$i" + 1) + done + + return 1 +} + +_rackcorp_validate() { + RACKCORP_APIUUID="${RACKCORP_APIUUID:-$(_readaccountconf_mutable RACKCORP_APIUUID)}" + if [ -z "$RACKCORP_APIUUID" ]; then + RACKCORP_APIUUID="" + _err "You require a RackCorp API UUID (export RACKCORP_APIUUID=\"\")" + _err "Please login to the portal and create an API key and try again." + return 1 + fi + + _saveaccountconf_mutable RACKCORP_APIUUID "$RACKCORP_APIUUID" + + RACKCORP_APISECRET="${RACKCORP_APISECRET:-$(_readaccountconf_mutable RACKCORP_APISECRET)}" + if [ -z "$RACKCORP_APISECRET" ]; then + RACKCORP_APISECRET="" + _err "You require a RackCorp API secret (export RACKCORP_APISECRET=\"\")" + _err "Please login to the portal and create an API key and try again." + return 1 + fi + + _saveaccountconf_mutable RACKCORP_APISECRET "$RACKCORP_APISECRET" + + return 0 +} +_rackcorp_api() { + _rackcorpcmd=$1 + _rackcorpinputdata=$2 + _debug cmd "$_rackcorpcmd $_rackcorpinputdata" + + export _H1="Accept: application/json" + response="$(_post "{\"APIUUID\":\"$RACKCORP_APIUUID\",\"APISECRET\":\"$RACKCORP_APISECRET\",\"cmd\":\"$_rackcorpcmd\",$_rackcorpinputdata}" "$RACKCORP_API_ENDPOINT" "" "POST")" + + if [ "$?" != "0" ]; then + _err "error $response" + return 1 + fi + _debug2 response "$response" + if _contains "$response" "\"code\":\"OK\""; then + _debug code "OK" + else + _debug code "FAILED" + response="" + return 1 + fi + return 0 +} diff --git a/dnsapi/dns_rackspace.sh b/dnsapi/dns_rackspace.sh new file mode 100644 index 00000000..b50d9168 --- /dev/null +++ b/dnsapi/dns_rackspace.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env sh +# +# +#RACKSPACE_Username="" +# +#RACKSPACE_Apikey="" + +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 + +######## Public functions ##################### +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_rackspace_add() { + fulldomain="$1" + _debug fulldomain="$fulldomain" + txtvalue="$2" + _debug txtvalue="$txtvalue" + _rackspace_check_auth || return 1 + _rackspace_check_rootzone || return 1 + _info "Creating TXT record." + if ! _rackspace_rest POST "$RACKSPACE_Tenant/domains/$_domain_id/records" "{\"records\":[{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":300}]}"; then + return 1 + fi + _debug2 response "$response" + if ! _contains "$response" "$txtvalue" >/dev/null; then + _err "Could not add TXT record." + return 1 + fi + return 0 +} + +#fulldomain txtvalue +dns_rackspace_rm() { + fulldomain=$1 + _debug fulldomain="$fulldomain" + txtvalue=$2 + _debug txtvalue="$txtvalue" + _rackspace_check_auth || return 1 + _rackspace_check_rootzone || return 1 + _info "Checking for TXT record." + if ! _get_recordid "$_domain_id" "$fulldomain" "$txtvalue"; then + _err "Could not get TXT record id." + return 1 + fi + if [ "$_dns_record_id" = "" ]; then + _err "TXT record not found." + return 1 + fi + _info "Removing TXT record." + if ! _delete_txt_record "$_domain_id" "$_dns_record_id"; then + _err "Could not remove TXT record $_dns_record_id." + 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_zone() { + domain="$1" + i=2 + 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 ! _rackspace_rest GET "$RACKSPACE_Tenant/domains/search?name=$h"; then + return 1 + fi + _debug2 response "$response" + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + # Response looks like: + # {"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) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_get_recordid() { + domainid="$1" + fulldomain="$2" + txtvalue="$3" + if ! _rackspace_rest GET "$RACKSPACE_Tenant/domains/$domainid/records?name=$fulldomain&type=TXT"; then + return 1 + fi + _debug response "$response" + if ! _contains "$response" "$txtvalue"; then + _dns_record_id=0 + return 0 + fi + _dns_record_id=$(echo "$response" | tr '{' "\n" | grep "\"data\":\"$txtvalue\"" | sed -n 's/^.*"id":"\([^"]*\)".*/\1/p') + _debug _dns_record_id "$_dns_record_id" + return 0 +} + +_delete_txt_record() { + domainid="$1" + _dns_record_id="$2" + if ! _rackspace_rest DELETE "$RACKSPACE_Tenant/domains/$domainid/records?id=$_dns_record_id"; then + return 1 + fi + _debug response "$response" + if ! _contains "$response" "RUNNING"; then + return 1 + fi + return 0 +} + +_rackspace_rest() { + m="$1" + ep="$2" + data="$3" + _debug ep "$ep" + export _H1="Accept: application/json" + export _H2="X-Auth-Token: $RACKSPACE_Token" + export _H3="X-Project-Id: $RACKSPACE_Tenant" + export _H4="Content-Type: application/json" + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$RACKSPACE_Endpoint/$ep" "" "$m")" + retcode=$? + else + _info "Getting $RACKSPACE_Endpoint/$ep" + response="$(_get "$RACKSPACE_Endpoint/$ep")" + retcode=$? + fi + + if [ "$retcode" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} + +_rackspace_authorization() { + export _H1="Content-Type: application/json" + data="{\"auth\":{\"RAX-KSKEY:apiKeyCredentials\":{\"username\":\"$RACKSPACE_Username\",\"apiKey\":\"$RACKSPACE_Apikey\"}}}" + _debug data "$data" + response="$(_post "$data" "https://identity.api.rackspacecloud.com/v2.0/tokens" "" "POST")" + retcode=$? + _debug2 response "$response" + if [ "$retcode" != "0" ]; then + _err "Authentication failed." + return 1 + fi + if _contains "$response" "token"; then + RACKSPACE_Token="$(echo "$response" | _normalizeJson | sed -n 's/^.*"token":{.*,"id":"\([^"]*\)",".*/\1/p')" + RACKSPACE_Tenant="$(echo "$response" | _normalizeJson | sed -n 's/^.*"token":{.*,"id":"\([^"]*\)"}.*/\1/p')" + _debug RACKSPACE_Token "$RACKSPACE_Token" + _debug RACKSPACE_Tenant "$RACKSPACE_Tenant" + fi + return 0 +} + +_rackspace_check_auth() { + # retrieve the rackspace creds + RACKSPACE_Username="${RACKSPACE_Username:-$(_readaccountconf_mutable RACKSPACE_Username)}" + RACKSPACE_Apikey="${RACKSPACE_Apikey:-$(_readaccountconf_mutable RACKSPACE_Apikey)}" + # check their vals for null + if [ -z "$RACKSPACE_Username" ] || [ -z "$RACKSPACE_Apikey" ]; then + RACKSPACE_Username="" + RACKSPACE_Apikey="" + _err "You didn't specify a Rackspace username and api key." + _err "Please set those values and try again." + return 1 + fi + # save the username and api key to the account conf file. + _saveaccountconf_mutable RACKSPACE_Username "$RACKSPACE_Username" + _saveaccountconf_mutable RACKSPACE_Apikey "$RACKSPACE_Apikey" + if [ -z "$RACKSPACE_Token" ]; then + _info "Getting authorization token." + if ! _rackspace_authorization; then + _err "Can not get token." + fi + fi +} + +_rackspace_check_rootzone() { + _debug "First detect the root zone" + if ! _get_root_zone "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" +} diff --git a/dnsapi/dns_rage4.sh b/dnsapi/dns_rage4.sh new file mode 100755 index 00000000..4af4541d --- /dev/null +++ b/dnsapi/dns_rage4.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env sh + +# +#RAGE4_TOKEN="sdfsdfsdfljlbjkljlkjsdfoiwje" +# +#RAGE4_USERNAME="xxxx@sss.com" + +RAGE4_Api="https://rage4.com/rapi/" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_rage4_add() { + fulldomain=$1 + txtvalue=$2 + + unquotedtxtvalue=$(echo "$txtvalue" | tr -d \") + + RAGE4_USERNAME="${RAGE4_USERNAME:-$(_readaccountconf_mutable RAGE4_USERNAME)}" + RAGE4_TOKEN="${RAGE4_TOKEN:-$(_readaccountconf_mutable RAGE4_TOKEN)}" + + if [ -z "$RAGE4_USERNAME" ] || [ -z "$RAGE4_TOKEN" ]; then + RAGE4_USERNAME="" + RAGE4_TOKEN="" + _err "You didn't specify a Rage4 api token and username yet." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable RAGE4_USERNAME "$RAGE4_USERNAME" + _saveaccountconf_mutable RAGE4_TOKEN "$RAGE4_TOKEN" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + + _rage4_rest "createrecord/?id=$_domain_id&name=$fulldomain&content=$unquotedtxtvalue&type=TXT&active=true&ttl=1" + return 0 +} + +#fulldomain txtvalue +dns_rage4_rm() { + fulldomain=$1 + txtvalue=$2 + + RAGE4_USERNAME="${RAGE4_USERNAME:-$(_readaccountconf_mutable RAGE4_USERNAME)}" + RAGE4_TOKEN="${RAGE4_TOKEN:-$(_readaccountconf_mutable RAGE4_TOKEN)}" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + + _debug "Getting txt records" + _rage4_rest "getrecords/?id=${_domain_id}" + + _record_id=$(echo "$response" | sed -rn 's/.*"id":([[:digit:]]+)[^\}]*'"$txtvalue"'.*/\1/p') + _rage4_rest "deleterecord/?id=${_record_id}" + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _domain=domain.com +# _domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$1 + + if ! _rage4_rest "getdomains"; then + return 1 + fi + _debug _get_root_domain "$domain" + + for line in $(echo "$response" | tr '}' '\n'); do + __domain=$(echo "$line" | sed -rn 's/.*"name":"([^"]*)",.*/\1/p') + __domain_id=$(echo "$line" | sed -rn 's/.*"id":([^,]*),.*/\1/p') + if [ "$domain" != "${domain%"$__domain"*}" ]; then + _domain_id="$__domain_id" + break + fi + done + + if [ -z "$_domain_id" ]; then + return 1 + fi + + return 0 +} + +_rage4_rest() { + ep="$1" + _debug "$ep" + + username_trimmed=$(echo "$RAGE4_USERNAME" | tr -d '"') + token_trimmed=$(echo "$RAGE4_TOKEN" | tr -d '"') + auth=$(printf '%s:%s' "$username_trimmed" "$token_trimmed" | _base64) + + export _H1="Content-Type: application/json" + export _H2="Authorization: Basic $auth" + + response="$(_get "$RAGE4_Api$ep")" + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_rcode0.sh b/dnsapi/dns_rcode0.sh new file mode 100755 index 00000000..d3f7f219 --- /dev/null +++ b/dnsapi/dns_rcode0.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env sh + +#Rcode0 API Integration +#https://my.rcodezero.at/api-doc +# +# log into https://my.rcodezero.at/enableapi and get your ACME API Token (the ACME API token has limited +# access to the REST calls needed for acme.sh only) +# +#RCODE0_URL="https://my.rcodezero.at" +#RCODE0_API_TOKEN="0123456789ABCDEF" +#RCODE0_TTL=60 + +DEFAULT_RCODE0_URL="https://my.rcodezero.at" +DEFAULT_RCODE0_TTL=60 + +######## Public functions ##################### +#Usage: add _acme-challenge.www.domain.com "123456789ABCDEF0000000000000000000000000000000000000" +#fulldomain +#txtvalue +dns_rcode0_add() { + fulldomain=$1 + txtvalue=$2 + + RCODE0_API_TOKEN="${RCODE0_API_TOKEN:-$(_readaccountconf_mutable RCODE0_API_TOKEN)}" + RCODE0_URL="${RCODE0_URL:-$(_readaccountconf_mutable RCODE0_URL)}" + RCODE0_TTL="${RCODE0_TTL:-$(_readaccountconf_mutable RCODE0_TTL)}" + + if [ -z "$RCODE0_URL" ]; then + RCODE0_URL="$DEFAULT_RCODE0_URL" + fi + + if [ -z "$RCODE0_API_TOKEN" ]; then + RCODE0_API_TOKEN="" + _err "Missing Rcode0 ACME API Token." + _err "Please login and create your token at httsp://my.rcodezero.at/enableapi and try again." + return 1 + fi + + if [ -z "$RCODE0_TTL" ]; then + RCODE0_TTL="$DEFAULT_RCODE0_TTL" + fi + + #save the token to the account conf file. + _saveaccountconf_mutable RCODE0_API_TOKEN "$RCODE0_API_TOKEN" + + if [ "$RCODE0_URL" != "$DEFAULT_RCODE0_URL" ]; then + _saveaccountconf_mutable RCODE0_URL "$RCODE0_URL" + fi + + if [ "$RCODE0_TTL" != "$DEFAULT_RCODE0_TTL" ]; then + _saveaccountconf_mutable RCODE0_TTL "$RCODE0_TTL" + fi + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "No 'MASTER' zone for $fulldomain found at RcodeZero Anycast." + return 1 + fi + _debug _domain "$_domain" + + _debug "Adding record" + + _record_string="" + _build_record_string "$txtvalue" + _list_existingchallenges + for oldchallenge in $_existing_challenges; do + _build_record_string "$oldchallenge" + done + + _debug "Challenges: $_existing_challenges" + + if [ -z "$_existing_challenges" ]; then + if ! _rcode0_rest "PATCH" "/api/v1/acme/zones/$_domain/rrsets" "[{\"changetype\": \"add\", \"name\": \"$fulldomain.\", \"type\": \"TXT\", \"ttl\": $RCODE0_TTL, \"records\": [$_record_string]}]"; then + _err "Add txt record error." + return 1 + fi + else + # try update in case a records exists (need for wildcard certs) + if ! _rcode0_rest "PATCH" "/api/v1/acme/zones/$_domain/rrsets" "[{\"changetype\": \"update\", \"name\": \"$fulldomain.\", \"type\": \"TXT\", \"ttl\": $RCODE0_TTL, \"records\": [$_record_string]}]"; then + _err "Set txt record error." + return 1 + fi + fi + + return 0 +} + +#fulldomain txtvalue +dns_rcode0_rm() { + fulldomain=$1 + txtvalue=$2 + + RCODE0_API_TOKEN="${RCODE0_API_TOKEN:-$(_readaccountconf_mutable RCODE0_API_TOKEN)}" + RCODE0_URL="${RCODE0_URL:-$(_readaccountconf_mutable RCODE0_URL)}" + RCODE0_TTL="${RCODE0_TTL:-$(_readaccountconf_mutable RCODE0_TTL)}" + + if [ -z "$RCODE0_URL" ]; then + RCODE0_URL="$DEFAULT_RCODE0_URL" + fi + + if [ -z "$RCODE0_API_TOKEN" ]; then + RCODE0_API_TOKEN="" + _err "Missing Rcode0 API Token." + _err "Please login and create your token at httsp://my.rcodezero.at/enableapi and try again." + return 1 + fi + + #save the api addr and key to the account conf file. + _saveaccountconf_mutable RCODE0_URL "$RCODE0_URL" + _saveaccountconf_mutable RCODE0_API_TOKEN "$RCODE0_API_TOKEN" + + if [ "$RCODE0_TTL" != "$DEFAULT_RCODE0_TTL" ]; then + _saveaccountconf_mutable RCODE0_TTL "$RCODE0_TTL" + fi + + if [ -z "$RCODE0_TTL" ]; then + RCODE0_TTL="$DEFAULT_RCODE0_TTL" + fi + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug "Remove record" + + #Enumerate existing acme challenges + _list_existingchallenges + + if _contains "$_existing_challenges" "$txtvalue"; then + #Delete all challenges (PowerDNS API does not allow to delete content) + if ! _rcode0_rest "PATCH" "/api/v1/acme/zones/$_domain/rrsets" "[{\"changetype\": \"delete\", \"name\": \"$fulldomain.\", \"type\": \"TXT\"}]"; then + _err "Delete txt record error." + return 1 + fi + _record_string="" + #If the only existing challenge was the challenge to delete: nothing to do + if ! [ "$_existing_challenges" = "$txtvalue" ]; then + for oldchallenge in $_existing_challenges; do + #Build up the challenges to re-add, ommitting the one what should be deleted + if ! [ "$oldchallenge" = "$txtvalue" ]; then + _build_record_string "$oldchallenge" + fi + done + #Recreate the existing challenges + if ! _rcode0_rest "PATCH" "/api/v1/acme/zones/$_domain/rrsets" "[{\"changetype\": \"update\", \"name\": \"$fulldomain.\", \"type\": \"TXT\", \"ttl\": $RCODE0_TTL, \"records\": [$_record_string]}]"; then + _err "Set txt record error." + return 1 + fi + fi + else + _info "Record not found, nothing to remove" + fi + + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + + _debug "try to find: $h" + if _rcode0_rest "GET" "/api/v1/acme/zones/$h"; then + if [ "$response" = "[\"found\"]" ]; then + _domain="$h" + if [ -z "$h" ]; then + _domain="=2E" + fi + return 0 + elif [ "$response" = "[\"not a master domain\"]" ]; then + return 1 + fi + fi + + if [ -z "$h" ]; then + return 1 + fi + i=$(_math $i + 1) + done + _debug "no matching domain for $domain found" + + return 1 +} + +_rcode0_rest() { + method=$1 + ep=$2 + data=$3 + + export _H1="Authorization: Bearer $RCODE0_API_TOKEN" + + if [ ! "$method" = "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$RCODE0_URL$ep" "" "$method")" + else + response="$(_get "$RCODE0_URL$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + + return 0 +} + +_build_record_string() { + _record_string="${_record_string:+${_record_string}, }{\"content\": \"\\\"${1}\\\"\", \"disabled\": false}" +} + +_list_existingchallenges() { + _rcode0_rest "GET" "/api/v1/acme/zones/$_domain/rrsets" + _existing_challenges=$(echo "$response" | _normalizeJson | _egrep_o "\"name\":\"${fulldomain}[^]]*}" | _egrep_o 'content\":\"\\"[^\\]*' | sed -n 's/^content":"\\"//p') + _debug2 "$_existing_challenges" +} diff --git a/dnsapi/dns_regru.sh b/dnsapi/dns_regru.sh new file mode 100644 index 00000000..8ff380f0 --- /dev/null +++ b/dnsapi/dns_regru.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env sh + +# +# REGRU_API_Username="test" +# +# REGRU_API_Password="test" +# + +REGRU_API_URL="https://api.reg.ru/api/regru2" + +######## Public functions ##################### + +dns_regru_add() { + fulldomain=$1 + txtvalue=$2 + + REGRU_API_Username="${REGRU_API_Username:-$(_readaccountconf_mutable REGRU_API_Username)}" + REGRU_API_Password="${REGRU_API_Password:-$(_readaccountconf_mutable REGRU_API_Password)}" + if [ -z "$REGRU_API_Username" ] || [ -z "$REGRU_API_Password" ]; then + REGRU_API_Username="" + REGRU_API_Password="" + _err "You don't specify regru password or username." + return 1 + fi + + _saveaccountconf_mutable REGRU_API_Username "$REGRU_API_Username" + _saveaccountconf_mutable REGRU_API_Password "$REGRU_API_Password" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain "$_domain" + + _subdomain=$(echo "$fulldomain" | sed -r "s/.$_domain//") + _debug _subdomain "$_subdomain" + + _info "Adding TXT record to ${fulldomain}" + _regru_rest POST "zone/add_txt" "input_data={%22username%22:%22${REGRU_API_Username}%22,%22password%22:%22${REGRU_API_Password}%22,%22domains%22:[{%22dname%22:%22${_domain}%22}],%22subdomain%22:%22${_subdomain}%22,%22text%22:%22${txtvalue}%22,%22output_content_type%22:%22plain%22}&input_format=json" + + if ! _contains "${response}" 'error'; then + return 0 + fi + _err "Could not create resource record, check logs" + _err "${response}" + return 1 +} + +dns_regru_rm() { + fulldomain=$1 + txtvalue=$2 + + REGRU_API_Username="${REGRU_API_Username:-$(_readaccountconf_mutable REGRU_API_Username)}" + REGRU_API_Password="${REGRU_API_Password:-$(_readaccountconf_mutable REGRU_API_Password)}" + if [ -z "$REGRU_API_Username" ] || [ -z "$REGRU_API_Password" ]; then + REGRU_API_Username="" + REGRU_API_Password="" + _err "You don't specify regru password or username." + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain "$_domain" + + _subdomain=$(echo "$fulldomain" | sed -r "s/.$_domain//") + _debug _subdomain "$_subdomain" + + _info "Deleting resource record $fulldomain" + _regru_rest POST "zone/remove_record" "input_data={%22username%22:%22${REGRU_API_Username}%22,%22password%22:%22${REGRU_API_Password}%22,%22domains%22:[{%22dname%22:%22${_domain}%22}],%22subdomain%22:%22${_subdomain}%22,%22content%22:%22${txtvalue}%22,%22record_type%22:%22TXT%22,%22output_content_type%22:%22plain%22}&input_format=json" + + if ! _contains "${response}" 'error'; then + return 0 + fi + _err "Could not delete resource record, check logs" + _err "${response}" + return 1 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _domain=domain.com +_get_root() { + domain=$1 + + _regru_rest POST "service/get_list" "username=${REGRU_API_Username}&password=${REGRU_API_Password}&output_format=xml&servtype=domain" + domains_list=$(echo "${response}" | grep dname | sed -r "s/.*dname=\"([^\"]+)\".*/\\1/g") + + for ITEM in ${domains_list}; do + IDN_ITEM=${ITEM} + case "${domain}" in + *${IDN_ITEM}*) + _domain="$(_idn "${ITEM}")" + _debug _domain "${_domain}" + return 0 + ;; + esac + done + + return 1 +} + +#returns +# response +_regru_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Content-Type: application/x-www-form-urlencoded" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$REGRU_API_URL/$ep" "" "$m")" + else + response="$(_get "$REGRU_API_URL/$ep?$data")" + fi + + _debug response "${response}" + return 0 +} diff --git a/dnsapi/dns_scaleway.sh b/dnsapi/dns_scaleway.sh new file mode 100755 index 00000000..a0a0f318 --- /dev/null +++ b/dnsapi/dns_scaleway.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env sh + +# Scaleway API +# https://developers.scaleway.com/en/products/domain/dns/api/ +# +# Requires Scaleway API token set in SCALEWAY_API_TOKEN + +######## Public functions ##################### + +SCALEWAY_API="https://api.scaleway.com/domain/v2beta1" + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_scaleway_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _scaleway_check_config; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + _scaleway_create_TXT_record "$_domain" "$_sub_domain" "$txtvalue" + if _contains "$response" "records"; then + return 0 + else + _err error "$response" + return 1 + fi + _info "Record added." + + return 0 +} + +dns_scaleway_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _scaleway_check_config; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Deleting record" + _scaleway_delete_TXT_record "$_domain" "$_sub_domain" "$txtvalue" + if _contains "$response" "records"; then + return 0 + else + _err error "$response" + return 1 + fi + _info "Record deleted." + + return 0 +} + +#################### Private functions below ################################## + +_scaleway_check_config() { + SCALEWAY_API_TOKEN="${SCALEWAY_API_TOKEN:-$(_readaccountconf_mutable SCALEWAY_API_TOKEN)}" + if [ -z "$SCALEWAY_API_TOKEN" ]; then + _err "No API key specified for Scaleway API." + _err "Create your key and export it as SCALEWAY_API_TOKEN" + return 1 + fi + if ! _scaleway_rest GET "dns-zones"; then + _err "Invalid API key specified for Scaleway API." + return 1 + fi + + _saveaccountconf_mutable SCALEWAY_API_TOKEN "$SCALEWAY_API_TOKEN" + + return 0 +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + _scaleway_rest GET "dns-zones/$h/records" + + if ! _contains "$response" "subdomain not found" >/dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + _err "Unable to retrive DNS zone matching this domain" + return 1 +} + +# this function add a TXT record +_scaleway_create_TXT_record() { + txt_zone=$1 + txt_name=$2 + txt_value=$3 + + _scaleway_rest PATCH "dns-zones/$txt_zone/records" "{\"return_all_records\":false,\"changes\":[{\"add\":{\"records\":[{\"name\":\"$txt_name\",\"data\":\"$txt_value\",\"type\":\"TXT\",\"ttl\":60}]}}]}" + + if _contains "$response" "records"; then + return 0 + else + _err "error1 $response" + return 1 + fi +} + +# this function delete a TXT record based on name and content +_scaleway_delete_TXT_record() { + txt_zone=$1 + txt_name=$2 + txt_value=$3 + + _scaleway_rest PATCH "dns-zones/$txt_zone/records" "{\"return_all_records\":false,\"changes\":[{\"delete\":{\"id_fields\":{\"name\":\"$txt_name\",\"data\":\"$txt_value\",\"type\":\"TXT\"}}}]}" + + if _contains "$response" "records"; then + return 0 + else + _err "error2 $response" + return 1 + fi +} + +_scaleway_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + _scaleway_url="$SCALEWAY_API/$ep" + _debug2 _scaleway_url "$_scaleway_url" + export _H1="x-auth-token: $SCALEWAY_API_TOKEN" + export _H2="Accept: application/json" + export _H3="Content-Type: application/json" + + if [ "$data" ] || [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$_scaleway_url" "" "$m")" + else + response="$(_get "$_scaleway_url")" + fi + if [ "$?" != "0" ] || _contains "$response" "denied_authentication" || _contains "$response" "Method not allowed" || _contains "$response" "json parse error: unexpected EOF"; then + _err "error $response" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_schlundtech.sh b/dnsapi/dns_schlundtech.sh new file mode 100644 index 00000000..399c50e0 --- /dev/null +++ b/dnsapi/dns_schlundtech.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env sh +# -*- mode: sh; tab-width: 2; indent-tabs-mode: s; coding: utf-8 -*- + +# Schlundtech DNS API +# Author: mod242 +# Created: 2019-40-29 +# Completly based on the autoDNS xml api wrapper by auerswald@gmail.com +# +# export SCHLUNDTECH_USER="username" +# export SCHLUNDTECH_PASSWORD="password" +# +# Usage: +# acme.sh --issue --dns dns_schlundtech -d example.com + +SCHLUNDTECH_API="https://gateway.schlundtech.de" + +# Arguments: +# txtdomain +# txt +dns_schlundtech_add() { + fulldomain="$1" + txtvalue="$2" + + SCHLUNDTECH_USER="${SCHLUNDTECH_USER:-$(_readaccountconf_mutable SCHLUNDTECH_USER)}" + SCHLUNDTECH_PASSWORD="${SCHLUNDTECH_PASSWORD:-$(_readaccountconf_mutable SCHLUNDTECH_PASSWORD)}" + + if [ -z "$SCHLUNDTECH_USER" ] || [ -z "$SCHLUNDTECH_PASSWORD" ]; then + _err "You didn't specify schlundtech user and password." + return 1 + fi + + _saveaccountconf_mutable SCHLUNDTECH_USER "$SCHLUNDTECH_USER" + _saveaccountconf_mutable SCHLUNDTECH_PASSWORD "$SCHLUNDTECH_PASSWORD" + + _debug "First detect the root zone" + + if ! _get_autodns_zone "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _zone "$_zone" + _debug _system_ns "$_system_ns" + + _info "Adding TXT record" + + autodns_response="$(_autodns_zone_update "$_zone" "$_sub_domain" "$txtvalue" "$_system_ns")" + + if [ "$?" -eq "0" ]; then + _info "Added, OK" + return 0 + fi + + return 1 +} + +# Arguments: +# txtdomain +# txt +dns_schlundtech_rm() { + fulldomain="$1" + txtvalue="$2" + + SCHLUNDTECH_USER="${SCHLUNDTECH_USER:-$(_readaccountconf_mutable SCHLUNDTECH_USER)}" + SCHLUNDTECH_PASSWORD="${SCHLUNDTECH_PASSWORD:-$(_readaccountconf_mutable SCHLUNDTECH_PASSWORD)}" + + if [ -z "$SCHLUNDTECH_USER" ] || [ -z "$SCHLUNDTECH_PASSWORD" ]; then + _err "You didn't specify schlundtech user and password." + return 1 + fi + + _debug "First detect the root zone" + + if ! _get_autodns_zone "$fulldomain"; then + _err "zone not found" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _zone "$_zone" + _debug _system_ns "$_system_ns" + + _info "Delete TXT record" + + autodns_response="$(_autodns_zone_cleanup "$_zone" "$_sub_domain" "$txtvalue" "$_system_ns")" + + if [ "$?" -eq "0" ]; then + _info "Deleted, OK" + return 0 + fi + + return 1 +} + +#################### Private functions below ################################## + +# Arguments: +# fulldomain +# Returns: +# _sub_domain=_acme-challenge.www +# _zone=domain.com +# _system_ns +_get_autodns_zone() { + domain="$1" + + i=2 + 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 + + autodns_response="$(_autodns_zone_inquire "$h")" + + if [ "$?" -ne "0" ]; then + _err "invalid domain" + return 1 + fi + + if _contains "$autodns_response" "1" >/dev/null; then + _zone="$(echo "$autodns_response" | _egrep_o '[^<]*' | cut -d '>' -f 2 | cut -d '<' -f 1)" + _system_ns="$(echo "$autodns_response" | _egrep_o '[^<]*' | cut -d '>' -f 2 | cut -d '<' -f 1)" + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done + + return 1 +} + +_build_request_auth_xml() { + printf " + %s + %s + 10 + " "$SCHLUNDTECH_USER" "$SCHLUNDTECH_PASSWORD" +} + +# Arguments: +# zone +_build_zone_inquire_xml() { + printf " + + %s + + 0205 + + 1 + 1 + + + name + eq + %s + + + " "$(_build_request_auth_xml)" "$1" +} + +# Arguments: +# zone +# subdomain +# txtvalue +# system_ns +_build_zone_update_xml() { + printf " + + %s + + 0202001 + + + %s + 600 + TXT + %s + + + + %s + %s + + + " "$(_build_request_auth_xml)" "$2" "$3" "$1" "$4" +} + +# Arguments: +# zone +_autodns_zone_inquire() { + request_data="$(_build_zone_inquire_xml "$1")" + autodns_response="$(_autodns_api_call "$request_data")" + ret="$?" + + printf "%s" "$autodns_response" + return "$ret" +} + +# Arguments: +# zone +# subdomain +# txtvalue +# system_ns +_autodns_zone_update() { + request_data="$(_build_zone_update_xml "$1" "$2" "$3" "$4")" + autodns_response="$(_autodns_api_call "$request_data")" + ret="$?" + + printf "%s" "$autodns_response" + return "$ret" +} + +# Arguments: +# zone +# subdomain +# txtvalue +# system_ns +_autodns_zone_cleanup() { + request_data="$(_build_zone_update_xml "$1" "$2" "$3" "$4")" + # replace 'rr_add>' with 'rr_rem>' in request_data + request_data="$(printf -- "%s" "$request_data" | sed 's/rr_add>/rr_rem>/g')" + autodns_response="$(_autodns_api_call "$request_data")" + ret="$?" + + printf "%s" "$autodns_response" + return "$ret" +} + +# Arguments: +# request_data +_autodns_api_call() { + request_data="$1" + + _debug request_data "$request_data" + + autodns_response="$(_post "$request_data" "$SCHLUNDTECH_API")" + ret="$?" + + _debug autodns_response "$autodns_response" + + if [ "$ret" -ne "0" ]; then + _err "error" + return 1 + fi + + if _contains "$autodns_response" "success" >/dev/null; then + _info "success" + printf "%s" "$autodns_response" + return 0 + fi + + return 1 +} diff --git a/dnsapi/dns_selectel.sh b/dnsapi/dns_selectel.sh index 94252d81..1b09882d 100644 --- a/dnsapi/dns_selectel.sh +++ b/dnsapi/dns_selectel.sh @@ -76,7 +76,7 @@ dns_selectel_rm() { return 1 fi - _record_seg="$(echo "$response" | _egrep_o "\"content\" *: *\"$txtvalue\"[^}]*}")" + _record_seg="$(echo "$response" | _egrep_o "[^{]*\"content\" *: *\"$txtvalue\"[^}]*}")" _debug2 "_record_seg" "$_record_seg" if [ -z "$_record_seg" ]; then _err "can not find _record_seg" @@ -120,7 +120,7 @@ _get_root() { return 1 fi - if _contains "$response" "\"name\": \"$h\","; then + if _contains "$response" "\"name\" *: *\"$h\","; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) _domain=$h _debug "Getting domain id for $h" diff --git a/dnsapi/dns_selfhost.sh b/dnsapi/dns_selfhost.sh new file mode 100644 index 00000000..a6ef1f94 --- /dev/null +++ b/dnsapi/dns_selfhost.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env sh +# +# Author: Marvin Edeler +# Report Bugs here: https://github.com/Marvo2011/acme.sh/issues/1 +# Last Edit: 17.02.2022 + +dns_selfhost_add() { + fulldomain=$1 + txt=$2 + _info "Calling acme-dns on selfhost" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txt" + + SELFHOSTDNS_UPDATE_URL="https://selfhost.de/cgi-bin/api.pl" + + # Get values, but don't save until we successfully validated + SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}" + SELFHOSTDNS_PASSWORD="${SELFHOSTDNS_PASSWORD:-$(_readaccountconf_mutable SELFHOSTDNS_PASSWORD)}" + # These values are domain dependent, so read them from there + SELFHOSTDNS_MAP="${SELFHOSTDNS_MAP:-$(_readdomainconf SELFHOSTDNS_MAP)}" + # Selfhost api can't dynamically add TXT record, + # so we have to store the last used RID of the domain to support a second RID for wildcard domains + # (format: 'fulldomainA:lastRid fulldomainB:lastRid ...') + SELFHOSTDNS_MAP_LAST_USED_INTERNAL=$(_readdomainconf SELFHOSTDNS_MAP_LAST_USED_INTERNAL) + + if [ -z "${SELFHOSTDNS_USERNAME:-}" ] || [ -z "${SELFHOSTDNS_PASSWORD:-}" ]; then + _err "SELFHOSTDNS_USERNAME and SELFHOSTDNS_PASSWORD must be set" + return 1 + fi + + # get the domain entry from SELFHOSTDNS_MAP + # only match full domains (at the beginning of the string or with a leading whitespace), + # e.g. don't match mytest.example.com or sub.test.example.com for test.example.com + # if the domain is defined multiple times only the last occurance will be matched + mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain)(:[[:digit:]]+)([:]?[[:digit:]]*)(.*)/\2\3\4/p") + _debug2 mapEntry "$mapEntry" + if test -z "$mapEntry"; then + _err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID" + return 1 + fi + + # get the RIDs from the map entry + rid1=$(echo "$mapEntry" | cut -d: -f2) + rid2=$(echo "$mapEntry" | cut -d: -f3) + + # read last used rid domain + lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain:[[:digit:]]+)(.*)/\2/p") + _debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry" + lastUsedRidForDomain=$(echo "$lastUsedRidForDomainEntry" | cut -d: -f2) + + rid="$rid1" + if [ "$lastUsedRidForDomain" = "$rid" ] && ! test -z "$rid2"; then + rid="$rid2" + fi + + _info "Trying to add $txt on selfhost for rid: $rid" + + data="?username=$SELFHOSTDNS_USERNAME&password=$SELFHOSTDNS_PASSWORD&rid=$rid&content=$txt" + response="$(_get "$SELFHOSTDNS_UPDATE_URL$data")" + + if ! echo "$response" | grep "200 OK" >/dev/null; then + _err "Invalid response of acme-dns for selfhost" + return 1 + fi + + # write last used rid domain + newLastUsedRidForDomainEntry="$fulldomain:$rid" + if ! test -z "$lastUsedRidForDomainEntry"; then + # replace last used rid entry for domain + SELFHOSTDNS_MAP_LAST_USED_INTERNAL=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed -n -E "s/$lastUsedRidForDomainEntry/$newLastUsedRidForDomainEntry/p") + else + # add last used rid entry for domain + if test -z "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL"; then + SELFHOSTDNS_MAP_LAST_USED_INTERNAL="$newLastUsedRidForDomainEntry" + else + SELFHOSTDNS_MAP_LAST_USED_INTERNAL="$SELFHOSTDNS_MAP_LAST_USED_INTERNAL $newLastUsedRidForDomainEntry" + fi + fi + + # Now that we know the values are good, save them + _saveaccountconf_mutable SELFHOSTDNS_USERNAME "$SELFHOSTDNS_USERNAME" + _saveaccountconf_mutable SELFHOSTDNS_PASSWORD "$SELFHOSTDNS_PASSWORD" + # These values are domain dependent, so store them there + _savedomainconf SELFHOSTDNS_MAP "$SELFHOSTDNS_MAP" + _savedomainconf SELFHOSTDNS_MAP_LAST_USED_INTERNAL "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" +} + +dns_selfhost_rm() { + fulldomain=$1 + txt=$2 + _debug fulldomain "$fulldomain" + _debug txtvalue "$txt" + _info "Creating and removing of records is not supported by selfhost API, will not delete anything." +} diff --git a/dnsapi/dns_servercow.sh b/dnsapi/dns_servercow.sh index be4e59da..52137905 100755 --- a/dnsapi/dns_servercow.sh +++ b/dnsapi/dns_servercow.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh ########## -# Custom servercow.de DNS API v1 for use with [acme.sh](https://github.com/Neilpang/acme.sh) +# Custom servercow.de DNS API v1 for use with [acme.sh](https://github.com/acmesh-official/acme.sh) # # Usage: # export SERVERCOW_API_Username=username @@ -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 new file mode 100644 index 00000000..6a8d0e18 --- /dev/null +++ b/dnsapi/dns_simply.sh @@ -0,0 +1,269 @@ +#!/usr/bin/env sh + +# API-integration for Simply.com (https://www.simply.com) + +#SIMPLY_AccountName="accountname" +#SIMPLY_ApiKey="apikey" +# +#SIMPLY_Api="https://api.simply.com/2/" +SIMPLY_Api_Default="https://api.simply.com/2" + +#This is used for determining success of REST call +SIMPLY_SUCCESS_CODE='"status":200' + +######## Public functions ##################### +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_simply_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _simply_load_config; then + return 1 + fi + + _simply_save_config + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + + if ! _simply_add_record "$_domain" "$_sub_domain" "$txtvalue"; then + _err "Could not add DNS record" + return 1 + fi + return 0 +} + +dns_simply_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _simply_load_config; then + return 1 + fi + + _simply_save_config + + _debug "Find the DNS zone" + + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug txtvalue "$txtvalue" + + _info "Getting all existing records" + + if ! _simply_get_all_records "$_domain"; then + _err "invalid domain" + return 1 + fi + + records=$(echo "$response" | tr '{' "\n" | grep 'record_id\|type\|data\|\name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ') + + nr_of_deleted_records=0 + _info "Fetching txt record" + + for record in $records; do + _debug record "$record" + + record_data=$(echo "$record" | sed -n "s/.*\"data\":\"\([^\"]*\)\".*/\1/p") + record_type=$(echo "$record" | sed -n "s/.*\"type\":\"\([^\"]*\)\".*/\1/p") + + _debug2 record_data "$record_data" + _debug2 record_type "$record_type" + + if [ "$record_data" = "$txtvalue" ] && [ "$record_type" = "TXT" ]; then + + record_id=$(echo "$record" | cut -d "," -f 1 | grep "record_id" | cut -d ":" -f 2) + + _info "Deleting record $record" + _debug2 record_id "$record_id" + + if [ "$record_id" -gt 0 ]; then + + if ! _simply_delete_record "$_domain" "$_sub_domain" "$record_id"; then + _err "Record with id $record_id could not be deleted" + return 1 + fi + + nr_of_deleted_records=1 + break + else + _err "Fetching record_id could not be done, this should not happen, exiting function. Failing record is $record" + break + fi + fi + + done + + if [ "$nr_of_deleted_records" -eq 0 ]; then + _err "No record deleted, the DNS record needs to be removed manually." + else + _info "Deleted $nr_of_deleted_records record" + fi + + return 0 +} + +#################### Private functions below ################################## + +_simply_load_config() { + SIMPLY_Api="${SIMPLY_Api:-$(_readaccountconf_mutable SIMPLY_Api)}" + SIMPLY_AccountName="${SIMPLY_AccountName:-$(_readaccountconf_mutable SIMPLY_AccountName)}" + SIMPLY_ApiKey="${SIMPLY_ApiKey:-$(_readaccountconf_mutable SIMPLY_ApiKey)}" + + if [ -z "$SIMPLY_Api" ]; then + SIMPLY_Api="$SIMPLY_Api_Default" + fi + + if [ -z "$SIMPLY_AccountName" ] || [ -z "$SIMPLY_ApiKey" ]; then + SIMPLY_AccountName="" + SIMPLY_ApiKey="" + + _err "A valid Simply API account and apikey not provided." + _err "Please provide a valid API user and try again." + + return 1 + fi + + return 0 +} + +_simply_save_config() { + if [ "$SIMPLY_Api" != "$SIMPLY_Api_Default" ]; then + _saveaccountconf_mutable SIMPLY_Api "$SIMPLY_Api" + fi + _saveaccountconf_mutable SIMPLY_AccountName "$SIMPLY_AccountName" + _saveaccountconf_mutable SIMPLY_ApiKey "$SIMPLY_ApiKey" +} + +_simply_get_all_records() { + domain=$1 + + if ! _simply_rest GET "my/products/$domain/dns/records/"; then + return 1 + fi + + return 0 +} + +_get_root() { + domain=$1 + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _simply_rest GET "my/products/$h/dns/"; then + return 1 + fi + + if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then + _debug "$h not found" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +_simply_add_record() { + domain=$1 + sub_domain=$2 + txtval=$3 + + data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 3600}" + + if ! _simply_rest POST "my/products/$domain/dns/records/" "$data"; then + _err "Adding record not successfull!" + 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 +} + +_simply_delete_record() { + domain=$1 + sub_domain=$2 + record_id=$3 + + _debug record_id "Delete record with id $record_id" + + if ! _simply_rest DELETE "my/products/$domain/dns/records/$record_id/"; then + _err "Deleting record not successfull!" + 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 +} + +_simply_rest() { + m=$1 + ep="$2" + data="$3" + + _debug2 data "$data" + _debug2 ep "$ep" + _debug2 m "$m" + + basicauth=$(printf "%s:%s" "$SIMPLY_AccountName" "$SIMPLY_ApiKey" | _base64) + + if [ "$basicauth" ]; then + export _H1="Authorization: Basic $basicauth" + fi + + export _H2="Content-Type: application/json" + + if [ "$m" != "GET" ]; then + response="$(_post "$data" "$SIMPLY_Api/$ep" "" "$m")" + else + response="$(_get "$SIMPLY_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + + response="$(echo "$response" | _normalizeJson)" + + _debug2 response "$response" + + if _contains "$response" "Invalid account authorization"; then + _err "It seems that your api key or accountnumber is not correct." + return 1 + fi + + return 0 +} diff --git a/dnsapi/dns_tencent.sh b/dnsapi/dns_tencent.sh new file mode 100644 index 00000000..2f8d3b67 --- /dev/null +++ b/dnsapi/dns_tencent.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env sh +Tencent_API="https://dnspod.tencentcloudapi.com" + +#Tencent_SecretId="AKIDz81d2cd22cdcdc2dcd1cc1d1A" +#Tencent_SecretKey="Gu5t9abcabcaabcbabcbbbcbcbbccbbcb" + +#Usage: dns_tencent_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_tencent_add() { + fulldomain=$1 + txtvalue=$2 + + Tencent_SecretId="${Tencent_SecretId:-$(_readaccountconf_mutable Tencent_SecretId)}" + Tencent_SecretKey="${Tencent_SecretKey:-$(_readaccountconf_mutable Tencent_SecretKey)}" + if [ -z "$Tencent_SecretId" ] || [ -z "$Tencent_SecretKey" ]; then + Tencent_SecretId="" + Tencent_SecretKey="" + _err "You don't specify tencent api SecretId and SecretKey yet." + return 1 + fi + + #save the api SecretId and SecretKey to the account conf file. + _saveaccountconf_mutable Tencent_SecretId "$Tencent_SecretId" + _saveaccountconf_mutable Tencent_SecretKey "$Tencent_SecretKey" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + return 1 + fi + + _debug "Add record" + _add_record_query "$_domain" "$_sub_domain" "$txtvalue" && _tencent_rest "CreateRecord" +} + +dns_tencent_rm() { + fulldomain=$1 + txtvalue=$2 + Tencent_SecretId="${Tencent_SecretId:-$(_readaccountconf_mutable Tencent_SecretId)}" + Tencent_SecretKey="${Tencent_SecretKey:-$(_readaccountconf_mutable Tencent_SecretKey)}" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + return 1 + fi + + _debug "Get record list" + attempt=1 + max_attempts=5 + while [ -z "$record_id" ] && [ "$attempt" -le $max_attempts ]; do + _check_exist_query "$_domain" "$_sub_domain" "$txtvalue" && _tencent_rest "DescribeRecordFilterList" + record_id="$(echo "$response" | _egrep_o "\"RecordId\":\s*[0-9]+" | _egrep_o "[0-9]+")" + _debug2 record_id "$record_id" + if [ -z "$record_id" ]; then + _debug "Due to TencentCloud API synchronization delay, record not found, waiting 10 seconds and retrying" + _sleep 10 + attempt=$(_math "$attempt + 1") + fi + done + + record_id="$(echo "$response" | _egrep_o "\"RecordId\":\s*[0-9]+" | _egrep_o "[0-9]+")" + _debug2 record_id "$record_id" + + if [ -z "$record_id" ]; then + _debug "record not found after $max_attempts attempts, skip" + else + _debug "Delete record" + _delete_record_query "$record_id" && _tencent_rest "DeleteRecord" + fi +} + +#################### Private functions below ################################## + +_get_root() { + domain=$1 + i=1 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + _describe_records_query "$h" "@" + if ! _tencent_rest "DescribeRecordList" "ignore"; then + return 1 + fi + + if _contains "$response" "\"TotalCount\":"; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _debug _sub_domain "$_sub_domain" + _domain="$h" + _debug _domain "$_domain" + return 0 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +_tencent_rest() { + action=$1 + service="dnspod" + payload="${query}" + timestamp=$(date -u +%s) + + token=$(tencent_signature_v3 $service "$action" "$payload" "$timestamp") + version="2021-03-23" + + if ! response="$(tencent_api_request $service $version "$action" "$payload" "$timestamp")"; then + _err "Error <$1>" + return 1 + fi + + _debug2 response "$response" + if [ -z "$2" ]; then + message="$(echo "$response" | _egrep_o "\"Message\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")" + if [ "$message" ]; then + _err "$message" + return 1 + fi + fi +} + +_add_record_query() { + query="{\"Domain\":\"$1\",\"SubDomain\":\"$2\",\"RecordType\":\"TXT\",\"RecordLineId\":\"0\",\"RecordLine\":\"0\",\"Value\":\"$3\",\"TTL\":600}" +} + +_describe_records_query() { + query="{\"Domain\":\"$1\",\"Limit\":3000}" +} + +_delete_record_query() { + query="{\"Domain\":\"$_domain\",\"RecordId\":$1}" +} + +_check_exist_query() { + _domain="$1" + _subdomain="$2" + _value="$3" + query="{\"Domain\":\"$_domain\",\"SubDomain\":\"$_subdomain\",\"RecordValue\":\"$_value\"}" +} + +# shell client for tencent cloud api v3 | @author: rehiy + +tencent_sha256() { + printf %b "$@" | _digest sha256 hex +} + +tencent_hmac_sha256() { + k=$1 + shift + hex_key=$(printf %b "$k" | _hex_dump | tr -d ' ') + printf %b "$@" | _hmac sha256 "$hex_key" hex +} + +tencent_hmac_sha256_hexkey() { + k=$1 + shift + printf %b "$@" | _hmac sha256 "$k" hex +} + +tencent_signature_v3() { + service=$1 + action=$(echo "$2" | _lower_case) + payload=${3:-'{}'} + timestamp=${4:-$(date +%s)} + + domain="$service.tencentcloudapi.com" + secretId=${Tencent_SecretId:-'tencent-cloud-secret-id'} + secretKey=${Tencent_SecretKey:-'tencent-cloud-secret-key'} + + algorithm='TC3-HMAC-SHA256' + date=$(date -u -d "@$timestamp" +%Y-%m-%d 2>/dev/null) + [ -z "$date" ] && date=$(date -u -r "$timestamp" +%Y-%m-%d) + + canonicalUri='/' + canonicalQuery='' + canonicalHeaders="content-type:application/json\nhost:$domain\nx-tc-action:$action\n" + + signedHeaders='content-type;host;x-tc-action' + canonicalRequest="POST\n$canonicalUri\n$canonicalQuery\n$canonicalHeaders\n$signedHeaders\n$(tencent_sha256 "$payload")" + + credentialScope="$date/$service/tc3_request" + stringToSign="$algorithm\n$timestamp\n$credentialScope\n$(tencent_sha256 "$canonicalRequest")" + + secretDate=$(tencent_hmac_sha256 "TC3$secretKey" "$date") + secretService=$(tencent_hmac_sha256_hexkey "$secretDate" "$service") + secretSigning=$(tencent_hmac_sha256_hexkey "$secretService" 'tc3_request') + signature=$(tencent_hmac_sha256_hexkey "$secretSigning" "$stringToSign") + + echo "$algorithm Credential=$secretId/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature" +} + +tencent_api_request() { + service=$1 + version=$2 + action=$3 + payload=${4:-'{}'} + timestamp=${5:-$(date +%s)} + + token=$(tencent_signature_v3 "$service" "$action" "$payload" "$timestamp") + + _H1="Content-Type: application/json" + _H2="Authorization: $token" + _H3="X-TC-Version: $version" + _H4="X-TC-Timestamp: $timestamp" + _H5="X-TC-Action: $action" + + _post "$payload" "$Tencent_API" "" "POST" "application/json" +} diff --git a/dnsapi/dns_transip.sh b/dnsapi/dns_transip.sh new file mode 100644 index 00000000..64a256ec --- /dev/null +++ b/dnsapi/dns_transip.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env sh +TRANSIP_Api_Url="https://api.transip.nl/v6" +TRANSIP_Token_Read_Only="false" +TRANSIP_Token_Expiration="30 minutes" +# You can't reuse a label token, so we leave this empty normally +TRANSIP_Token_Label="" + +######## Public functions ##################### +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_transip_add() { + fulldomain="$1" + _debug fulldomain="$fulldomain" + txtvalue="$2" + _debug txtvalue="$txtvalue" + _transip_setup "$fulldomain" || return 1 + _info "Creating TXT record." + if ! _transip_rest POST "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":300}}"; then + _err "Could not add TXT record." + return 1 + fi + return 0 +} + +dns_transip_rm() { + fulldomain=$1 + _debug fulldomain="$fulldomain" + txtvalue=$2 + _debug txtvalue="$txtvalue" + _transip_setup "$fulldomain" || return 1 + _info "Removing TXT record." + if ! _transip_rest DELETE "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":300}}"; then + _err "Could not remove TXT record $_sub_domain for $domain" + return 1 + fi + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain="$1" + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + + if [ -z "$h" ]; then + #not valid + return 1 + fi + + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + + if _transip_rest GET "domains/$h/dns" && _contains "$response" "dnsEntries"; then + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done + _err "Unable to parse this domain" + return 1 +} + +_transip_rest() { + m="$1" + ep="$2" + data="$3" + _debug ep "$ep" + export _H1="Accept: application/json" + export _H2="Authorization: Bearer $_token" + export _H4="Content-Type: application/json" + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$TRANSIP_Api_Url/$ep" "" "$m")" + retcode=$? + else + response="$(_get "$TRANSIP_Api_Url/$ep")" + retcode=$? + fi + + if [ "$retcode" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} + +_transip_get_token() { + nonce=$(echo "TRANSIP$(_time)" | _digest sha1 hex | cut -c 1-32) + _debug nonce "$nonce" + + # make IP whitelisting configurable + TRANSIP_Token_Global_Key="${TRANSIP_Token_Global_Key:-$(_readaccountconf_mutable TRANSIP_Token_Global_Key)}" + _saveaccountconf_mutable TRANSIP_Token_Global_Key "$TRANSIP_Token_Global_Key" + + data="{\"login\":\"${TRANSIP_Username}\",\"nonce\":\"${nonce}\",\"read_only\":\"${TRANSIP_Token_Read_Only}\",\"expiration_time\":\"${TRANSIP_Token_Expiration}\",\"label\":\"${TRANSIP_Token_Label}\",\"global_key\":\"${TRANSIP_Token_Global_Key:-false}\"}" + _debug data "$data" + + #_signature=$(printf "%s" "$data" | openssl dgst -sha512 -sign "$TRANSIP_Key_File" | _base64) + _signature=$(printf "%s" "$data" | _sign "$TRANSIP_Key_File" "sha512") + _debug2 _signature "$_signature" + + export _H1="Signature: $_signature" + export _H2="Content-Type: application/json" + + response="$(_post "$data" "$TRANSIP_Api_Url/auth" "" "POST")" + retcode=$? + _debug2 response "$response" + if [ "$retcode" != "0" ]; then + _err "Authentication failed." + return 1 + fi + if _contains "$response" "token"; then + _token="$(echo "$response" | _normalizeJson | sed -n 's/^{"token":"\(.*\)"}/\1/p')" + _debug _token "$_token" + return 0 + fi + return 1 +} + +_transip_setup() { + fulldomain=$1 + + # retrieve the transip creds + TRANSIP_Username="${TRANSIP_Username:-$(_readaccountconf_mutable TRANSIP_Username)}" + TRANSIP_Key_File="${TRANSIP_Key_File:-$(_readaccountconf_mutable TRANSIP_Key_File)}" + # check their vals for null + if [ -z "$TRANSIP_Username" ] || [ -z "$TRANSIP_Key_File" ]; then + TRANSIP_Username="" + TRANSIP_Key_File="" + _err "You didn't specify a TransIP username and api key file location" + _err "Please set those values and try again." + return 1 + fi + # save the username and api key to the account conf file. + _saveaccountconf_mutable TRANSIP_Username "$TRANSIP_Username" + _saveaccountconf_mutable TRANSIP_Key_File "$TRANSIP_Key_File" + + # download key file if it's an URL + if _startswith "$TRANSIP_Key_File" "http"; then + _debug "download transip key file" + TRANSIP_Key_URL=$TRANSIP_Key_File + TRANSIP_Key_File="$(_mktemp)" + chmod 600 "$TRANSIP_Key_File" + if ! _get "$TRANSIP_Key_URL" >"$TRANSIP_Key_File"; then + _err "Error getting key file from : $TRANSIP_Key_URL" + return 1 + fi + fi + + if [ -f "$TRANSIP_Key_File" ]; then + if ! grep "BEGIN PRIVATE KEY" "$TRANSIP_Key_File" >/dev/null 2>&1; then + _err "Key file doesn't seem to be a valid key: ${TRANSIP_Key_File}" + return 1 + fi + else + _err "Can't read private key file: ${TRANSIP_Key_File}" + return 1 + fi + + if [ -z "$_token" ]; then + if ! _transip_get_token; then + _err "Can not get token." + return 1 + fi + fi + + if [ -n "${TRANSIP_Key_URL}" ]; then + _debug "delete transip key file" + rm "${TRANSIP_Key_File}" + TRANSIP_Key_File=$TRANSIP_Key_URL + fi + + _get_root "$fulldomain" || return 1 + + return 0 +} diff --git a/dnsapi/dns_udr.sh b/dnsapi/dns_udr.sh new file mode 100644 index 00000000..caada826 --- /dev/null +++ b/dnsapi/dns_udr.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env sh + +# united-domains Reselling (https://www.ud-reselling.com/) DNS API +# Author: Andreas Scherer (https://github.com/andischerer) +# Created: 2021-02-01 +# +# Set the environment variables as below: +# +# export UDR_USER="your_username_goes_here" +# export UDR_PASS="some_password_goes_here" +# + +UDR_API="https://api.domainreselling.de/api/call.cgi" +UDR_TTL="30" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "some_long_string_of_characters_go_here_from_lets_encrypt" +dns_udr_add() { + fulldomain=$1 + txtvalue=$2 + + UDR_USER="${UDR_USER:-$(_readaccountconf_mutable UDR_USER)}" + UDR_PASS="${UDR_PASS:-$(_readaccountconf_mutable UDR_PASS)}" + if [ -z "$UDR_USER" ] || [ -z "$UDR_PASS" ]; then + UDR_USER="" + UDR_PASS="" + _err "You didn't specify an UD-Reselling username and password yet" + return 1 + fi + # save the username and password to the account conf file. + _saveaccountconf_mutable UDR_USER "$UDR_USER" + _saveaccountconf_mutable UDR_PASS "$UDR_PASS" + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _dnszone "${_dnszone}" + + _debug "Getting txt records" + if ! _udr_rest "QueryDNSZoneRRList" "dnszone=${_dnszone}"; then + return 1 + fi + + rr="${fulldomain}. ${UDR_TTL} IN TXT ${txtvalue}" + _debug resource_record "${rr}" + if _contains "$response" "$rr" >/dev/null; then + _err "Error, it would appear that this record already exists. Please review existing TXT records for this domain." + return 1 + fi + + _info "Adding record" + if ! _udr_rest "UpdateDNSZone" "dnszone=${_dnszone}&addrr0=${rr}"; then + _err "Adding the record did not succeed, please verify/check." + return 1 + fi + + _info "Added, OK" + return 0 +} + +dns_udr_rm() { + fulldomain=$1 + txtvalue=$2 + + UDR_USER="${UDR_USER:-$(_readaccountconf_mutable UDR_USER)}" + UDR_PASS="${UDR_PASS:-$(_readaccountconf_mutable UDR_PASS)}" + if [ -z "$UDR_USER" ] || [ -z "$UDR_PASS" ]; then + UDR_USER="" + UDR_PASS="" + _err "You didn't specify an UD-Reselling username and password yet" + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _dnszone "${_dnszone}" + + _debug "Getting txt records" + if ! _udr_rest "QueryDNSZoneRRList" "dnszone=${_dnszone}"; then + return 1 + fi + + rr="${fulldomain}. ${UDR_TTL} IN TXT ${txtvalue}" + _debug resource_record "${rr}" + if _contains "$response" "$rr" >/dev/null; then + if ! _udr_rest "UpdateDNSZone" "dnszone=${_dnszone}&delrr0=${rr}"; then + _err "Deleting the record did not succeed, please verify/check." + return 1 + fi + _info "Removed, OK" + return 0 + else + _info "Text record is not present, will not delete anything." + return 0 + fi +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + + if ! _udr_rest "QueryDNSZoneList" ""; 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}" "${h}." >/dev/null; then + _dnszone=$(echo "$response" | _egrep_o "${h}") + if [ "$_dnszone" ]; then + return 0 + fi + return 1 + fi + i=$(_math "$i" + 1) + done + return 1 +} + +_udr_rest() { + if [ -n "$2" ]; then + data="command=$1&$2" + else + data="command=$1" + fi + + _debug data "${data}" + response="$(_post "${data}" "${UDR_API}?s_login=${UDR_USER}&s_pw=${UDR_PASS}" "" "POST")" + + _code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + _description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + + _debug response_code "$_code" + _debug response_description "$_description" + + if [ ! "$_code" = "200" ]; then + _err "DNS-API-Error: $_description" + return 1 + fi + + return 0 +} diff --git a/dnsapi/dns_ultra.sh b/dnsapi/dns_ultra.sh new file mode 100644 index 00000000..0f26bd97 --- /dev/null +++ b/dnsapi/dns_ultra.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env sh + +# +# ULTRA_USR="your_user_goes_here" +# +# ULTRA_PWD="some_password_goes_here" + +ULTRA_API="https://api.ultradns.com/v3/" +ULTRA_AUTH_API="https://api.ultradns.com/v2/" + +#Usage: add _acme-challenge.www.domain.com "some_long_string_of_characters_go_here_from_lets_encrypt" +dns_ultra_add() { + fulldomain=$1 + txtvalue=$2 + export txtvalue + ULTRA_USR="${ULTRA_USR:-$(_readaccountconf_mutable ULTRA_USR)}" + ULTRA_PWD="${ULTRA_PWD:-$(_readaccountconf_mutable ULTRA_PWD)}" + if [ -z "$ULTRA_USR" ] || [ -z "$ULTRA_PWD" ]; then + ULTRA_USR="" + ULTRA_PWD="" + _err "You didn't specify an UltraDNS username and password yet" + return 1 + fi + # save the username and password to the account conf file. + _saveaccountconf_mutable ULTRA_USR "$ULTRA_USR" + _saveaccountconf_mutable ULTRA_PWD "$ULTRA_PWD" + _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" + _ultra_rest GET "zones/${_domain_id}/rrsets/TXT?q=value:${fulldomain}" + if printf "%s" "$response" | grep \"totalCount\" >/dev/null; then + _err "Error, it would appear that this record already exists. Please review existing TXT records for this domain." + return 1 + fi + + _info "Adding record" + if _ultra_rest POST "zones/$_domain_id/rrsets/TXT/${_sub_domain}" '{"ttl":300,"rdata":["'"${txtvalue}"'"]}'; then + if _contains "$response" "Successful"; then + _info "Added, OK" + return 0 + elif _contains "$response" "Resource Record of type 16 with these attributes already exists"; then + _info "Already exists, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + +} + +dns_ultra_rm() { + fulldomain=$1 + txtvalue=$2 + export txtvalue + ULTRA_USR="${ULTRA_USR:-$(_readaccountconf_mutable ULTRA_USR)}" + ULTRA_PWD="${ULTRA_PWD:-$(_readaccountconf_mutable ULTRA_PWD)}" + if [ -z "$ULTRA_USR" ] || [ -z "$ULTRA_PWD" ]; then + ULTRA_USR="" + ULTRA_PWD="" + _err "You didn't specify an UltraDNS username and password yet" + return 1 + fi + + _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" + _ultra_rest GET "zones/${_domain_id}/rrsets?q=kind:RECORDS+owner:${_sub_domain}" + + if ! printf "%s" "$response" | grep \"resultInfo\" >/dev/null; then + _err "There was an error in obtaining the resource records for ${_domain_id}" + return 1 + fi + + count=$(echo "$response" | _egrep_o "\"returnedCount\":[^,]*" | cut -d: -f2 | cut -d'}' -f1) + _debug count "${count}" + if [ "${count}" = "" ]; then + _info "Text record is not present, will not delete anything." + else + if ! _ultra_rest DELETE "zones/$_domain_id/rrsets/TXT/${_sub_domain}" '{"ttl":300,"rdata":["'"${txtvalue}"'"]}'; then + _err "Deleting the record did not succeed, please verify/check." + return 1 + fi + _contains "$response" "" + 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 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + _debug response "$response" + if [ -z "$h" ]; then + #not valid + return 1 + fi + if ! _ultra_rest GET "zones"; then + return 1 + fi + if _contains "${response}" "${h}." >/dev/null; then + _domain_id=$(echo "$response" | _egrep_o "${h}" | head -1) + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="${h}" + _debug sub_domain "${_sub_domain}" + _debug domain "${_domain}" + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_ultra_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + if [ -z "$AUTH_TOKEN" ]; then + _ultra_login + fi + _debug TOKEN "$AUTH_TOKEN" + + export _H1="Content-Type: application/json" + export _H2="Authorization: Bearer $AUTH_TOKEN" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$ULTRA_API$ep" "" "$m")" + else + response="$(_get "$ULTRA_API$ep")" + fi +} + +_ultra_login() { + export _H1="" + export _H2="" + AUTH_TOKEN=$(_post "grant_type=password&username=${ULTRA_USR}&password=${ULTRA_PWD}" "${ULTRA_AUTH_API}authorization/token" | cut -d, -f3 | cut -d\" -f4) + export AUTH_TOKEN +} diff --git a/dnsapi/dns_unoeuro.sh b/dnsapi/dns_unoeuro.sh index 9132f136..13ba8a00 100644 --- a/dnsapi/dns_unoeuro.sh +++ b/dnsapi/dns_unoeuro.sh @@ -5,7 +5,7 @@ # #UNO_User="UExxxxxx" -Uno_Api="https://api.unoeuro.com/1" +Uno_Api="https://api.simply.com/1" ######## Public functions ##################### @@ -24,12 +24,6 @@ dns_unoeuro_add() { return 1 fi - if ! _contains "$UNO_User" "UE"; then - _err "It seems that the UNO_User=$UNO_User is not a valid username." - _err "Please check and retry." - return 1 - fi - #save the api key and email to the account conf file. _saveaccountconf_mutable UNO_Key "$UNO_Key" _saveaccountconf_mutable UNO_User "$UNO_User" @@ -52,7 +46,7 @@ dns_unoeuro_add() { fi _info "Adding record" - if _uno_rest POST "my/products/$h/dns/records" "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":120}"; then + if _uno_rest POST "my/products/$h/dns/records" "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":120,\"priority\":0}"; then if _contains "$response" "\"status\": 200" >/dev/null; then _info "Added, OK" return 0 diff --git a/dnsapi/dns_variomedia.sh b/dnsapi/dns_variomedia.sh new file mode 100644 index 00000000..aa743807 --- /dev/null +++ b/dnsapi/dns_variomedia.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env sh + +# +#VARIOMEDIA_API_TOKEN=000011112222333344445555666677778888 + +VARIOMEDIA_API="https://api.variomedia.de" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_variomedia_add() { + fulldomain=$1 + txtvalue=$2 + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + VARIOMEDIA_API_TOKEN="${VARIOMEDIA_API_TOKEN:-$(_readaccountconf_mutable VARIOMEDIA_API_TOKEN)}" + if test -z "$VARIOMEDIA_API_TOKEN"; then + VARIOMEDIA_API_TOKEN="" + _err 'VARIOMEDIA_API_TOKEN was not exported' + return 1 + fi + + _saveaccountconf_mutable VARIOMEDIA_API_TOKEN "$VARIOMEDIA_API_TOKEN" + + _debug 'First detect the root zone' + if ! _get_root "$fulldomain"; then + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + if ! _variomedia_rest POST "dns-records" "{\"data\": {\"type\": \"dns-record\", \"attributes\": {\"record_type\": \"TXT\", \"name\": \"$_sub_domain\", \"domain\": \"$_domain\", \"data\": \"$txtvalue\", \"ttl\":300}}}"; then + _err "$response" + return 1 + fi + + _debug2 _response "$response" + return 0 +} + +#fulldomain txtvalue +dns_variomedia_rm() { + fulldomain=$1 + txtvalue=$2 + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + VARIOMEDIA_API_TOKEN="${VARIOMEDIA_API_TOKEN:-$(_readaccountconf_mutable VARIOMEDIA_API_TOKEN)}" + if test -z "$VARIOMEDIA_API_TOKEN"; then + VARIOMEDIA_API_TOKEN="" + _err 'VARIOMEDIA_API_TOKEN was not exported' + return 1 + fi + + _saveaccountconf_mutable VARIOMEDIA_API_TOKEN "$VARIOMEDIA_API_TOKEN" + + _debug 'First detect the root zone' + if ! _get_root "$fulldomain"; then + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug 'Getting txt records' + + if ! _variomedia_rest GET "dns-records?filter[domain]=$_domain"; then + _err 'Error' + return 1 + fi + + _record_id="$(echo "$response" | sed -E 's/,"tags":\[[^]]*\]//g' | cut -d '[' -f2 | cut -d']' -f1 | sed 's/},[ \t]*{/\},§\{/g' | tr § '\n' | grep "$_sub_domain" | grep -- "$txtvalue" | sed 's/^{//;s/}[,]?$//' | tr , '\n' | tr -d '\"' | grep ^id | cut -d : -f2 | tr -d ' ')" + _debug _record_id "$_record_id" + if [ "$_record_id" ]; then + _info "Successfully retrieved the record id for ACME challenge." + else + _info "Empty record id, it seems no such record." + return 0 + fi + + if ! _variomedia_rest DELETE "/dns-records/$_record_id"; then + _err "$response" + return 1 + fi + + _debug2 _response "$response" + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + return 1 + fi + + if ! _variomedia_rest GET "domains/$h"; then + return 1 + fi + + if _contains "$response" "\"id\":\"$h\""; then + _sub_domain=$(printf "%s" "$domain" | cut -d '.' -f 1-$p) + _domain="$h" + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_variomedia_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Authorization: token $VARIOMEDIA_API_TOKEN" + export _H2="Content-Type: application/vnd.api+json" + export _H3="Accept: application/vnd.variomedia.v1+json" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$VARIOMEDIA_API/$ep" "" "$m")" + else + response="$(_get "$VARIOMEDIA_API/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "Error $ep" + return 1 + fi + + _debug2 response "$response" + 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_vercel.sh b/dnsapi/dns_vercel.sh new file mode 100644 index 00000000..7bf6b0e5 --- /dev/null +++ b/dnsapi/dns_vercel.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env sh + +# Vercel DNS API +# +# This is your API token which can be acquired on the account page. +# https://vercel.com/account/tokens +# +# VERCEL_TOKEN="sdfsdfsdfljlbjkljlkjsdfoiwje" + +VERCEL_API="https://api.vercel.com" + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_vercel_add() { + fulldomain=$1 + txtvalue=$2 + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + VERCEL_TOKEN="${VERCEL_TOKEN:-$(_readaccountconf_mutable VERCEL_TOKEN)}" + + if [ -z "$VERCEL_TOKEN" ]; then + VERCEL_TOKEN="" + _err "You have not set the Vercel API token yet." + _err "Please visit https://vercel.com/account/tokens to generate it." + return 1 + fi + + _saveaccountconf_mutable VERCEL_TOKEN "$VERCEL_TOKEN" + + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding record" + if _vercel_rest POST "v2/domains/$_domain/records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\"}"; then + if printf -- "%s" "$response" | grep "\"uid\":\"" >/dev/null; then + _info "Added" + return 0 + else + _err "Unexpected response while adding text record." + return 1 + fi + fi + _err "Add txt record error." +} + +dns_vercel_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _vercel_rest GET "v2/domains/$_domain/records" + + count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$_sub_domain\",[^{]*\"type\":\"TXT\"" | wc -l | tr -d " ") + + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + _record_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"slug\":\"[^,]*\",\"name\":\"$_sub_domain\",[^{]*\"type\":\"TXT\",\"value\":\"$txtvalue\"" | cut -d: -f2 | cut -d, -f1 | tr -d '"') + + if [ "$_record_id" ]; then + echo "$_record_id" | while read -r item; do + if _vercel_rest DELETE "v2/domains/$_domain/records/$item"; then + _info "removed record" "$item" + return 0 + else + _err "failed to remove record" "$item" + return 1 + fi + done + fi + fi +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain="$1" + ep="$2" + i=1 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + #not valid + return 1 + fi + + if ! _vercel_rest GET "v4/domains/$h"; then + return 1 + fi + + if _contains "$response" "\"name\":\"$h\"" >/dev/null; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_vercel_rest() { + m="$1" + ep="$2" + data="$3" + + path="$VERCEL_API/$ep" + + export _H1="Content-Type: application/json" + export _H2="Authorization: Bearer $VERCEL_TOKEN" + + if [ "$m" != "GET" ]; then + _secure_debug2 data "$data" + response="$(_post "$data" "$path" "" "$m")" + else + response="$(_get "$path")" + fi + _ret="$?" + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug "http response code $_code" + _secure_debug2 response "$response" + if [ "$_ret" != "0" ]; then + _err "error $ep" + return 1 + fi + + response="$(printf "%s" "$response" | _normalizeJson)" + return 0 +} diff --git a/dnsapi/dns_vscale.sh b/dnsapi/dns_vscale.sh index e50b7d8b..d717d6e2 100755 --- a/dnsapi/dns_vscale.sh +++ b/dnsapi/dns_vscale.sh @@ -102,7 +102,7 @@ _get_root() { return 1 fi - hostedzone="$(echo "$response" | _egrep_o "{.*\"name\":\s*\"$h\".*}")" + hostedzone="$(echo "$response" | tr "{" "\n" | _egrep_o "\"name\":\s*\"$h\".*}")" if [ "$hostedzone" ]; then _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) if [ "$_domain_id" ]; then diff --git a/dnsapi/dns_vultr.sh b/dnsapi/dns_vultr.sh new file mode 100644 index 00000000..54e5b6ce --- /dev/null +++ b/dnsapi/dns_vultr.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env sh + +# +#VULTR_API_KEY=000011112222333344445555666677778888 + +VULTR_Api="https://api.vultr.com/v2" + +######## Public functions ##################### +# +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_vultr_add() { + fulldomain=$1 + txtvalue=$2 + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + VULTR_API_KEY="${VULTR_API_KEY:-$(_readaccountconf_mutable VULTR_API_KEY)}" + if test -z "$VULTR_API_KEY"; then + VULTR_API_KEY='' + _err 'VULTR_API_KEY was not exported' + return 1 + fi + + _saveaccountconf_mutable VULTR_API_KEY "$VULTR_API_KEY" + + _debug 'First detect the root zone' + if ! _get_root "$fulldomain"; then + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug 'Getting txt records' + _vultr_rest GET "domains/$_domain/records" + + if printf "%s\n" "$response" | grep -- "\"type\":\"TXT\",\"name\":\"$fulldomain\"" >/dev/null; then + _err 'Error' + return 1 + fi + + if ! _vultr_rest POST "domains/$_domain/records" "{\"name\":\"$_sub_domain\",\"data\":\"$txtvalue\",\"type\":\"TXT\"}"; then + _err "$response" + return 1 + fi + + _debug2 _response "$response" + return 0 +} + +#fulldomain txtvalue +dns_vultr_rm() { + fulldomain=$1 + txtvalue=$2 + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + VULTR_API_KEY="${VULTR_API_KEY:-$(_readaccountconf_mutable VULTR_API_KEY)}" + if test -z "$VULTR_API_KEY"; then + VULTR_API_KEY="" + _err 'VULTR_API_KEY was not exported' + return 1 + fi + + _saveaccountconf_mutable VULTR_API_KEY "$VULTR_API_KEY" + + _debug 'First detect the root zone' + if ! _get_root "$fulldomain"; then + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug 'Getting txt records' + _vultr_rest GET "domains/$_domain/records" + + 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 'id' | cut -d : -f 2 | tr -d '"')" + _debug _record_id "$_record_id" + if [ "$_record_id" ]; then + _info "Successfully retrieved the record id for ACME challenge." + else + _info "Empty record id, it seems no such record." + return 0 + fi + + if ! _vultr_rest DELETE "domains/$_domain/records/$_record_id"; then + _err "$response" + return 1 + fi + + _debug2 _response "$response" + 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 + while true; do + _domain=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$_domain" + if [ -z "$_domain" ]; then + return 1 + fi + + if ! _vultr_rest GET "domains"; then + return 1 + fi + + if printf "%s\n" "$response" | grep -E '^\{.*\}' >/dev/null; then + if _contains "$response" "\"domain\":\"$_domain\""; then + _sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")" + return 0 + else + _debug "Go to next level of $_domain" + fi + else + _err "$response" + return 1 + fi + i=$(_math "$i" + 1) + done + + return 1 +} + +_vultr_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + api_key_trimmed=$(echo "$VULTR_API_KEY" | tr -d '"') + + export _H1="Authorization: Bearer $api_key_trimmed" + export _H2='Content-Type: application/json' + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$VULTR_Api/$ep" "" "$m")" + else + response="$(_get "$VULTR_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "Error $ep" + return 1 + fi + + _debug2 response "$response" + return 0 +} 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_west_cn.sh b/dnsapi/dns_west_cn.sh new file mode 100644 index 00000000..d0bb7d49 --- /dev/null +++ b/dnsapi/dns_west_cn.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env sh + +# West.cn Domain api +#WEST_Username="username" +#WEST_Key="sADDsdasdgdsf" +#Set key at https://www.west.cn/manager/API/APIconfig.asp + +REST_API="https://api.west.cn/API/v2" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_west_cn_add() { + fulldomain=$1 + txtvalue=$2 + + WEST_Username="${WEST_Username:-$(_readaccountconf_mutable WEST_Username)}" + WEST_Key="${WEST_Key:-$(_readaccountconf_mutable WEST_Key)}" + if [ -z "$WEST_Username" ] || [ -z "$WEST_Key" ]; then + WEST_Username="" + WEST_Key="" + _err "You don't specify west api key and username yet." + _err "Please set you key and try again." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable WEST_Username "$WEST_Username" + _saveaccountconf_mutable WEST_Key "$WEST_Key" + + add_record "$fulldomain" "$txtvalue" +} + +#Usage: rm _acme-challenge.www.domain.com +dns_west_cn_rm() { + fulldomain=$1 + txtvalue=$2 + + WEST_Username="${WEST_Username:-$(_readaccountconf_mutable WEST_Username)}" + WEST_Key="${WEST_Key:-$(_readaccountconf_mutable WEST_Key)}" + + if ! _rest POST "domain/dns/" "act=dnsrec.list&username=$WEST_Username&apikey=$WEST_Key&domain=$fulldomain&hostname=$fulldomain&record_type=TXT"; then + _err "dnsrec.list error." + return 1 + fi + + if _contains "$response" 'no records'; then + _info "Don't need to remove." + return 0 + fi + + record_id=$(echo "$response" | tr "{" "\n" | grep -- "$txtvalue" | grep '^"record_id"' | cut -d : -f 2 | cut -d ',' -f 1) + _debug record_id "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id." + return 1 + fi + + if ! _rest POST "domain/dns/" "act=dnsrec.remove&username=$WEST_Username&apikey=$WEST_Key&domain=$fulldomain&hostname=$fulldomain&record_id=$record_id"; then + _err "dnsrec.remove error." + return 1 + fi + + _contains "$response" "success" +} + +#add the txt record. +#usage: add fulldomain txtvalue +add_record() { + fulldomain=$1 + txtvalue=$2 + + _info "Adding record" + + if ! _rest POST "domain/dns/" "act=dnsrec.add&username=$WEST_Username&apikey=$WEST_Key&domain=$fulldomain&hostname=$fulldomain&record_type=TXT&record_value=$txtvalue"; then + return 1 + fi + + _contains "$response" "success" +} + +#Usage: method URI data +_rest() { + m="$1" + ep="$2" + data="$3" + _debug "$ep" + url="$REST_API/$ep" + + _debug url "$url" + + if [ "$m" = "GET" ]; then + response="$(_get "$url" | tr -d '\r')" + else + _debug2 data "$data" + response="$(_post "$data" "$url" | tr -d '\r')" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_world4you.sh b/dnsapi/dns_world4you.sh new file mode 100644 index 00000000..dfda4efd --- /dev/null +++ b/dnsapi/dns_world4you.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env sh + +# World4You - www.world4you.com +# Lorenz Stechauner, 2020 - https://www.github.com/NerLOR + +WORLD4YOU_API="https://my.world4you.com/en" +PAKETNR='' +TLD='' +RECORD='' + +################ Public functions ################ + +# Usage: dns_world4you_add +dns_world4you_add() { + fqdn=$(echo "$1" | _lower_case) + value="$2" + _info "Using world4you to add record" + _debug fulldomain "$fqdn" + _debug txtvalue "$value" + + _login + if [ "$?" != 0 ]; then + return 1 + fi + + export _H1="Cookie: W4YSESSID=$sessid" + form=$(_get "$WORLD4YOU_API/") + _get_paketnr "$fqdn" "$form" + paketnr="$PAKETNR" + if [ -z "$paketnr" ]; then + _err "Unable to parse paketnr" + return 3 + fi + _debug paketnr "$paketnr" + + export _H1="Cookie: W4YSESSID=$sessid" + form=$(_get "$WORLD4YOU_API/$paketnr/dns") + formiddp=$(echo "$form" | grep 'AddDnsRecordForm\[uniqueFormIdDP\]' | sed 's/^.*name="AddDnsRecordForm\[uniqueFormIdDP\]" 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" + return 3 + fi + + _resethttp + export ACME_HTTP_NO_REDIRECTS=1 + 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 + + if _contains "$(_head_n 1 <"$HTTP_HEADER")" '302'; then + res=$(_get "$WORLD4YOU_API/$paketnr/dns") + if _contains "$res" "successfully"; then + return 0 + else + msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "]*>[^<]" | sed 's/<[^>]*>//g' | sed 's/^\s*//g') + if [ "$msg" = '' ]; then + _err "Unable to add record: Unknown error" + echo "$ret" >'error-01.html' + echo "$res" >'error-02.html' + _err "View error-01.html and error-02.html for debugging" + else + _err "Unable to add record: my.world4you.com: $msg" + fi + return 1 + fi + else + msg=$(echo "$ret" | grep '"form-error-message"' | sed 's/^.*
\([^<]*\)<\/div>.*$/\1/') + _err "Unable to add record: my.world4you.com: $msg" + return 1 + fi +} + +# Usage: dns_world4you_rm +dns_world4you_rm() { + fqdn=$(echo "$1" | _lower_case) + value="$2" + _info "Using world4you to remove record" + _debug fulldomain "$fqdn" + _debug txtvalue "$value" + + _login + if [ "$?" != 0 ]; then + return 1 + fi + + export _H1="Cookie: W4YSESSID=$sessid" + form=$(_get "$WORLD4YOU_API/") + _get_paketnr "$fqdn" "$form" + paketnr="$PAKETNR" + if [ -z "$paketnr" ]; then + _err "Unable to parse paketnr" + return 3 + fi + _debug paketnr "$paketnr" + + form=$(_get "$WORLD4YOU_API/$paketnr/dns") + formiddp=$(echo "$form" | grep 'DeleteDnsRecordForm\[uniqueFormIdDP\]' | sed 's/^.*name="DeleteDnsRecordForm\[uniqueFormIdDP\]" 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" + return 3 + fi + + recordid=$(printf "TXT:%s.:\"%s\"" "$fqdn" "$value" | _base64) + _debug recordid "$recordid" + + _resethttp + export ACME_HTTP_NO_REDIRECTS=1 + body="DeleteDnsRecordForm[recordId]=$recordid&DeleteDnsRecordForm[uniqueFormIdDP]=$formiddp&DeleteDnsRecordForm[_token]=$form_token" + _info "Removing record..." + ret=$(_post "$body" "$WORLD4YOU_API/$paketnr/dns/record/delete" '' POST 'application/x-www-form-urlencoded') + _resethttp + + if _contains "$(_head_n 1 <"$HTTP_HEADER")" '302'; then + res=$(_get "$WORLD4YOU_API/$paketnr/dns") + if _contains "$res" "successfully"; then + return 0 + else + msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "]*>[^<]" | sed 's/<[^>]*>//g' | sed 's/^\s*//g') + if [ "$msg" = '' ]; then + _err "Unable to remove record: Unknown error" + echo "$ret" >'error-01.html' + echo "$res" >'error-02.html' + _err "View error-01.html and error-02.html for debugging" + else + _err "Unable to remove record: my.world4you.com: $msg" + fi + return 1 + fi + else + msg=$(echo "$ret" | grep "form-error-message" | sed 's/^.*
\([^<]*\)<\/div>.*$/\1/') + _err "Unable to remove record: my.world4you.com: $msg" + return 1 + fi +} + +################ Private functions ################ + +# Usage: _login +_login() { + WORLD4YOU_USERNAME="${WORLD4YOU_USERNAME:-$(_readaccountconf_mutable WORLD4YOU_USERNAME)}" + WORLD4YOU_PASSWORD="${WORLD4YOU_PASSWORD:-$(_readaccountconf_mutable WORLD4YOU_PASSWORD)}" + + if [ -z "$WORLD4YOU_USERNAME" ] || [ -z "$WORLD4YOU_PASSWORD" ]; then + WORLD4YOU_USERNAME="" + WORLD4YOU_PASSWORD="" + _err "You didn't specify world4you username and password yet." + _err "Usage: export WORLD4YOU_USERNAME=" + _err "Usage: export WORLD4YOU_PASSWORD=" + return 1 + fi + + _saveaccountconf_mutable WORLD4YOU_USERNAME "$WORLD4YOU_USERNAME" + _saveaccountconf_mutable WORLD4YOU_PASSWORD "$WORLD4YOU_PASSWORD" + + _resethttp + export ACME_HTTP_NO_REDIRECTS=1 + page=$(_get "$WORLD4YOU_API/login") + _resethttp + + if _contains "$(_head_n 1 <"$HTTP_HEADER")" '302'; then + _info "Already logged in" + _parse_sessid + return 0 + fi + + _info "Logging in..." + + username="$WORLD4YOU_USERNAME" + password="$WORLD4YOU_PASSWORD" + csrf_token=$(echo "$page" | grep '_csrf_token' | sed 's/^.*]*value=\"\([^"]*\)\".*$/\1/') + _parse_sessid + + export _H1="Cookie: W4YSESSID=$sessid" + export _H2="X-Requested-With: XMLHttpRequest" + body="_username=$username&_password=$password&_csrf_token=$csrf_token" + ret=$(_post "$body" "$WORLD4YOU_API/login" '' POST 'application/x-www-form-urlencoded') + unset _H2 + + _debug ret "$ret" + if _contains "$ret" "\"success\":true"; then + _info "Successfully logged in" + _parse_sessid + else + msg=$(echo "$ret" | sed 's/^.*"message":"\([^\"]*\)".*$/\1/') + _err "Unable to log in: my.world4you.com: $msg" + return 1 + fi +} + +# Usage: _get_paketnr +_get_paketnr() { + fqdn="$1" + form="$2" + + domains=$(echo "$form" | grep '